├── sum.go ├── service_protocol.go ├── packet_interface.go ├── .golangci.yml ├── constants ├── station_url_flag.go ├── prudp_packet_types.go ├── prudp_packet_flags.go ├── nat_mapping_properties.go ├── nat_filtering_properties.go ├── station_url_type.go ├── stream_type.go └── signature_method.go ├── compression ├── algorithm.go ├── dummy.go ├── lzo.go └── zlib.go ├── kerberos_test.go ├── encryption ├── algorithm.go ├── dummy.go ├── rc4.go └── quazal_rc4.go ├── connection_interface.go ├── byte_stream_settings.go ├── endpoint_interface.go ├── types ├── data_holder.go ├── rv_type.go ├── structure.go ├── writable.go ├── bool.go ├── int8.go ├── uint8.go ├── float.go ├── int16.go ├── int32.go ├── int64.go ├── double.go ├── uint16.go ├── uint32.go ├── readable.go ├── data.go ├── buffer.go ├── qbuffer.go ├── uint64.go ├── class_version_container.go ├── string.go ├── pid.go ├── result_range.go ├── qresult.go ├── variant.go ├── any_object_holder.go ├── map.go ├── rv_connection_data.go ├── quuid.go ├── list.go └── datetime.go ├── init.go ├── counter.go ├── .gitignore ├── timeout.go ├── socket_connection.go ├── prudp_v1_settings.go ├── connection_state.go ├── hpp_client.go ├── error.go ├── virtual_port.go ├── test ├── generate_ticket.go ├── main.go ├── hpp.go └── auth.go ├── prudp_v0_settings.go ├── go.mod ├── packet_dispatch_queue_test.go ├── account.go ├── sliding_window.go ├── prudp_packet_interface.go ├── packet_dispatch_queue.go ├── rtt.go ├── websocket_server.go ├── library_version.go ├── mutex_map.go ├── timeout_manager.go ├── mutex_slice.go ├── byte_stream_out.go ├── hpp_packet.go ├── README.md ├── stream_settings.go ├── byte_stream_in.go ├── kerberos.go ├── go.sum └── hpp_server.go /sum.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import "golang.org/x/exp/constraints" 4 | 5 | func sum[T, O constraints.Integer](data []T) O { 6 | var result O 7 | for _, b := range data { 8 | result += O(b) 9 | } 10 | return result 11 | } 12 | -------------------------------------------------------------------------------- /service_protocol.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | // ServiceProtocol represents a NEX service capable of handling PRUDP/HPP packets 4 | type ServiceProtocol interface { 5 | HandlePacket(packet PacketInterface) 6 | Endpoint() EndpointInterface 7 | SetEndpoint(endpoint EndpointInterface) 8 | } 9 | -------------------------------------------------------------------------------- /packet_interface.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | // PacketInterface defines all the methods a packet for both PRUDP and HPP should have 4 | type PacketInterface interface { 5 | Sender() ConnectionInterface 6 | Payload() []byte 7 | SetPayload(payload []byte) 8 | RMCMessage() *RMCMessage 9 | SetRMCMessage(message *RMCMessage) 10 | } 11 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - revive 4 | linters-settings: 5 | revive: 6 | ignore-generated-header: true 7 | severity: error 8 | rules: 9 | - name: exported 10 | - name: unexported-naming 11 | - name: unexported-return 12 | - name: package-comments 13 | - name: var-naming 14 | issues: 15 | exclude-use-default: false -------------------------------------------------------------------------------- /constants/station_url_flag.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // StationURLFlag is an enum of flags used by the StationURL "type" parameter. 4 | type StationURLFlag uint8 5 | 6 | const ( 7 | // StationURLFlagBehindNAT indicates the user is behind NAT 8 | StationURLFlagBehindNAT StationURLFlag = iota + 1 9 | 10 | // StationURLFlagPublic indicates the station is a public address 11 | StationURLFlagPublic 12 | ) 13 | -------------------------------------------------------------------------------- /compression/algorithm.go: -------------------------------------------------------------------------------- 1 | // Package compression provides a set of compression algorithms found 2 | // in several versions of Rendez-Vous for compressing large payloads 3 | package compression 4 | 5 | // Algorithm defines all the methods a compression algorithm should have 6 | type Algorithm interface { 7 | Compress(payload []byte) ([]byte, error) 8 | Decompress(payload []byte) ([]byte, error) 9 | Copy() Algorithm 10 | } 11 | -------------------------------------------------------------------------------- /kerberos_test.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/PretendoNetwork/nex-go/v2/types" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestDeriveGuestKey(t *testing.T) { 12 | pid := types.NewPID(100) 13 | password := []byte("MMQea3n!fsik") 14 | result := DeriveKerberosKey(pid, password) 15 | assert.Equal(t, "9ef318f0a170fb46aab595bf9644f9e1", hex.EncodeToString(result)) 16 | } 17 | -------------------------------------------------------------------------------- /encryption/algorithm.go: -------------------------------------------------------------------------------- 1 | // Package encryption provides a set of encryption algorithms found 2 | // in several versions of Rendez-Vous for encrypting payloads 3 | package encryption 4 | 5 | // Algorithm defines all the methods a compression algorithm should have 6 | type Algorithm interface { 7 | Key() []byte 8 | SetKey(key []byte) error 9 | Encrypt(payload []byte) ([]byte, error) 10 | Decrypt(payload []byte) ([]byte, error) 11 | Copy() Algorithm 12 | } 13 | -------------------------------------------------------------------------------- /connection_interface.go: -------------------------------------------------------------------------------- 1 | // Package nex provides a collection of utility structs, functions, and data types for making NEX/QRV servers 2 | package nex 3 | 4 | import ( 5 | "net" 6 | 7 | "github.com/PretendoNetwork/nex-go/v2/types" 8 | ) 9 | 10 | // ConnectionInterface defines all the methods a connection should have regardless of server type 11 | type ConnectionInterface interface { 12 | Endpoint() EndpointInterface 13 | Address() net.Addr 14 | PID() types.PID 15 | SetPID(pid types.PID) 16 | } 17 | -------------------------------------------------------------------------------- /byte_stream_settings.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | // ByteStreamSettings defines some settings for how a ByteStream should handle certain data types 4 | type ByteStreamSettings struct { 5 | StringLengthSize int 6 | PIDSize int 7 | UseStructureHeader bool 8 | } 9 | 10 | // NewByteStreamSettings returns a new ByteStreamSettings 11 | func NewByteStreamSettings() *ByteStreamSettings { 12 | return &ByteStreamSettings{ 13 | StringLengthSize: 2, 14 | PIDSize: 4, 15 | UseStructureHeader: false, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /endpoint_interface.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | // EndpointInterface defines all the methods an endpoint should have regardless of type 4 | type EndpointInterface interface { 5 | AccessKey() string 6 | SetAccessKey(accessKey string) 7 | Send(packet PacketInterface) 8 | LibraryVersions() *LibraryVersions 9 | ByteStreamSettings() *ByteStreamSettings 10 | SetByteStreamSettings(settings *ByteStreamSettings) 11 | UseVerboseRMC() bool // TODO - Move this to a RMCSettings struct? 12 | EnableVerboseRMC(enabled bool) 13 | EmitError(err *Error) 14 | } 15 | -------------------------------------------------------------------------------- /types/data_holder.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // DataInterface defines an interface to track types which have Data anywhere 4 | // in their parent tree. 5 | type DataInterface interface { 6 | HoldableObject 7 | DataObjectID() RVType // Returns the object identifier of the type embedding Data 8 | } 9 | 10 | // DataHolder is an AnyObjectHolder for types which embed Data 11 | type DataHolder = AnyObjectHolder[DataInterface] 12 | 13 | // NewDataHolder returns a new DataHolder 14 | func NewDataHolder() DataHolder { 15 | return DataHolder{} 16 | } 17 | -------------------------------------------------------------------------------- /constants/prudp_packet_types.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // SynPacket is the ID for the PRUDP Syn Packet type 5 | SynPacket uint16 = 0x0 6 | 7 | // ConnectPacket is the ID for the PRUDP Connect Packet type 8 | ConnectPacket uint16 = 0x1 9 | 10 | // DataPacket is the ID for the PRUDP Data Packet type 11 | DataPacket uint16 = 0x2 12 | 13 | // DisconnectPacket is the ID for the PRUDP Disconnect Packet type 14 | DisconnectPacket uint16 = 0x3 15 | 16 | // PingPacket is the ID for the PRUDP Ping Packet type 17 | PingPacket uint16 = 0x4 18 | ) 19 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import ( 4 | "github.com/PretendoNetwork/nex-go/v2/types" 5 | "github.com/PretendoNetwork/plogger-go" 6 | ) 7 | 8 | var logger = plogger.NewLogger() 9 | 10 | func init() { 11 | initResultCodes() 12 | 13 | types.RegisterVariantType(1, types.NewInt64(0)) 14 | types.RegisterVariantType(2, types.NewDouble(0)) 15 | types.RegisterVariantType(3, types.NewBool(false)) 16 | types.RegisterVariantType(4, types.NewString("")) 17 | types.RegisterVariantType(5, types.NewDateTime(0)) 18 | types.RegisterVariantType(6, types.NewUInt64(0)) 19 | } 20 | -------------------------------------------------------------------------------- /constants/prudp_packet_flags.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // PacketFlagAck is the ID for the PRUDP Ack Flag 5 | PacketFlagAck uint16 = 0x1 6 | 7 | // PacketFlagReliable is the ID for the PRUDP Reliable Flag 8 | PacketFlagReliable uint16 = 0x2 9 | 10 | // PacketFlagNeedsAck is the ID for the PRUDP NeedsAck Flag 11 | PacketFlagNeedsAck uint16 = 0x4 12 | 13 | // PacketFlagHasSize is the ID for the PRUDP HasSize Flag 14 | PacketFlagHasSize uint16 = 0x8 15 | 16 | // PacketFlagMultiAck is the ID for the PRUDP MultiAck Flag 17 | PacketFlagMultiAck uint16 = 0x200 18 | ) 19 | -------------------------------------------------------------------------------- /counter.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import ( 4 | "golang.org/x/exp/constraints" 5 | ) 6 | 7 | type numeric interface { 8 | constraints.Integer | constraints.Float | constraints.Complex 9 | } 10 | 11 | // Counter represents an incremental counter of a specific numeric type 12 | type Counter[T numeric] struct { 13 | Value T 14 | } 15 | 16 | // Next increments the counter by 1 and returns the new value 17 | func (c *Counter[T]) Next() T { 18 | c.Value++ 19 | return c.Value 20 | } 21 | 22 | // NewCounter returns a new Counter, with a starting number 23 | func NewCounter[T numeric](start T) *Counter[T] { 24 | return &Counter[T]{Value: start} 25 | } 26 | -------------------------------------------------------------------------------- /compression/dummy.go: -------------------------------------------------------------------------------- 1 | package compression 2 | 3 | // Dummy does no compression. Payloads are returned as-is 4 | type Dummy struct{} 5 | 6 | // Compress does nothing 7 | func (d *Dummy) Compress(payload []byte) ([]byte, error) { 8 | return payload, nil 9 | } 10 | 11 | // Decompress does nothing 12 | func (d *Dummy) Decompress(payload []byte) ([]byte, error) { 13 | return payload, nil 14 | } 15 | 16 | // Copy returns a copy of the algorithm 17 | func (d *Dummy) Copy() Algorithm { 18 | return NewDummyCompression() 19 | } 20 | 21 | // NewDummyCompression returns a new instance of the Dummy compression 22 | func NewDummyCompression() *Dummy { 23 | return &Dummy{} 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # custom 25 | .vscode 26 | .idea 27 | .env 28 | build 29 | log 30 | *.pem 31 | *.key -------------------------------------------------------------------------------- /constants/nat_mapping_properties.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // NATMappingProperties is an implementation of the nn::nex::NATProperties::MappingProperties enum. 4 | // 5 | // NATMappingProperties is used to indicate the NAT mapping properties of the users router. 6 | // 7 | // See https://datatracker.ietf.org/doc/html/rfc4787 for more details 8 | type NATMappingProperties uint8 9 | 10 | const ( 11 | // UnknownNATMapping indicates the NAT type could not be identified 12 | UnknownNATMapping NATMappingProperties = iota 13 | 14 | // EIMNATMapping indicates endpoint-independent mapping 15 | EIMNATMapping 16 | 17 | // EDMNATMapping indicates endpoint-dependent mapping 18 | EDMNATMapping 19 | ) 20 | -------------------------------------------------------------------------------- /timeout.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // Timeout is an implementation of rdv::Timeout. 9 | // Used to hold state related to resend timeouts on a packet 10 | type Timeout struct { 11 | timeout time.Duration 12 | ctx context.Context 13 | cancel context.CancelFunc 14 | } 15 | 16 | // SetRTO sets the timeout field on this instance 17 | func (t *Timeout) SetRTO(timeout time.Duration) { 18 | t.timeout = timeout 19 | } 20 | 21 | // GetRTO gets the timeout field of this instance 22 | func (t *Timeout) RTO() time.Duration { 23 | return t.timeout 24 | } 25 | 26 | // NewTimeout creates a new Timeout 27 | func NewTimeout() *Timeout { 28 | return &Timeout{} 29 | } 30 | -------------------------------------------------------------------------------- /constants/nat_filtering_properties.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // NATFilteringProperties is an implementation of the nn::nex::NATProperties::FilteringProperties enum. 4 | // 5 | // NATFilteringProperties is used to indicate the NAT filtering properties of the users router. 6 | // 7 | // See https://datatracker.ietf.org/doc/html/rfc4787 for more details 8 | type NATFilteringProperties uint8 9 | 10 | const ( 11 | // UnknownNATFiltering indicates the NAT type could not be identified 12 | UnknownNATFiltering NATFilteringProperties = iota 13 | 14 | // PIFNATFiltering indicates port-independent filtering 15 | PIFNATFiltering 16 | 17 | // PDFNATFiltering indicates port-dependent filtering 18 | PDFNATFiltering 19 | ) 20 | -------------------------------------------------------------------------------- /constants/station_url_type.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // StationURLType is an implementation of the nn::nex::StationURL::URLType enum. 4 | // 5 | // StationURLType is used to indicate the type of connection to use when contacting a station. 6 | type StationURLType uint8 7 | 8 | const ( 9 | // UnknownStationURLType indicates an unknown URL type 10 | UnknownStationURLType StationURLType = iota 11 | 12 | // StationURLPRUDP indicates the station should be contacted with a standard PRUDP connection 13 | StationURLPRUDP 14 | 15 | // StationURLPRUDPS indicates the station should be contacted with a secure PRUDP connection 16 | StationURLPRUDPS 17 | 18 | // StationURLUDP indicates the station should be contacted with raw UDP data. Used for custom protocols 19 | StationURLUDP 20 | ) 21 | -------------------------------------------------------------------------------- /socket_connection.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/lxzan/gws" 7 | ) 8 | 9 | // SocketConnection represents a single open socket. 10 | // A single socket may have many PRUDP connections open on it. 11 | type SocketConnection struct { 12 | Server *PRUDPServer // * PRUDP server the socket is connected to 13 | Address net.Addr // * Sockets address 14 | WebSocketConnection *gws.Conn // * Only used in PRUDPLite 15 | } 16 | 17 | // NewSocketConnection creates a new SocketConnection 18 | func NewSocketConnection(server *PRUDPServer, address net.Addr, webSocketConnection *gws.Conn) *SocketConnection { 19 | return &SocketConnection{ 20 | Server: server, 21 | Address: address, 22 | WebSocketConnection: webSocketConnection, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /prudp_v1_settings.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import "net" 4 | 5 | // TODO - We can also breakout the decoding/encoding functions here too, but that would require getters and setters for all packet fields 6 | 7 | // PRUDPV1Settings defines settings for how to handle aspects of PRUDPv1 packets 8 | type PRUDPV1Settings struct { 9 | LegacyConnectionSignature bool 10 | ConnectionSignatureCalculator func(packet *PRUDPPacketV1, addr net.Addr) ([]byte, error) 11 | SignatureCalculator func(packet *PRUDPPacketV1, sessionKey, connectionSignature []byte) []byte 12 | } 13 | 14 | // NewPRUDPV1Settings returns a new PRUDPV1Settings 15 | func NewPRUDPV1Settings() *PRUDPV1Settings { 16 | return &PRUDPV1Settings{ 17 | LegacyConnectionSignature: false, 18 | ConnectionSignatureCalculator: defaultPRUDPv1ConnectionSignature, 19 | SignatureCalculator: defaultPRUDPv1CalculateSignature, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /connection_state.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | // ConnectionState is an implementation of the nn::nex::EndPoint::_ConnectionState enum. 4 | // 5 | // The state represents a PRUDP clients connection state. The original Rendez-Vous 6 | // library supports states 0-6, though NEX only supports 0-4. The remaining 2 are 7 | // unknown 8 | type ConnectionState uint8 9 | 10 | const ( 11 | // StateNotConnected indicates the client has not established a full PRUDP connection 12 | StateNotConnected ConnectionState = iota 13 | 14 | // StateConnecting indicates the client is attempting to establish a PRUDP connection 15 | StateConnecting 16 | 17 | // StateConnected indicates the client has established a full PRUDP connection 18 | StateConnected 19 | 20 | // StateDisconnecting indicates the client is disconnecting from a PRUDP connection. Currently unused 21 | StateDisconnecting 22 | 23 | // StateFaulty indicates the client connection is faulty. Currently unused 24 | StateFaulty 25 | ) 26 | -------------------------------------------------------------------------------- /encryption/dummy.go: -------------------------------------------------------------------------------- 1 | package encryption 2 | 3 | // Dummy does no encryption. Payloads are returned as-is 4 | type Dummy struct { 5 | key []byte 6 | } 7 | 8 | // Key returns the crypto key 9 | func (d *Dummy) Key() []byte { 10 | return d.key 11 | } 12 | 13 | // SetKey sets the crypto key 14 | func (d *Dummy) SetKey(key []byte) error { 15 | d.key = key 16 | 17 | return nil 18 | } 19 | 20 | // Encrypt does nothing 21 | func (d *Dummy) Encrypt(payload []byte) ([]byte, error) { 22 | return payload, nil 23 | } 24 | 25 | // Decrypt does nothing 26 | func (d *Dummy) Decrypt(payload []byte) ([]byte, error) { 27 | return payload, nil 28 | } 29 | 30 | // Copy returns a copy of the algorithm while retaining it's state 31 | func (d *Dummy) Copy() Algorithm { 32 | copied := NewDummyEncryption() 33 | 34 | copied.key = d.key 35 | 36 | return copied 37 | } 38 | 39 | // NewDummyEncryption returns a new instance of the Dummy encryption 40 | func NewDummyEncryption() *Dummy { 41 | return &Dummy{ 42 | key: make([]byte, 0), 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /hpp_client.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/PretendoNetwork/nex-go/v2/types" 7 | ) 8 | 9 | // HPPClient represents a single HPP client 10 | type HPPClient struct { 11 | address *net.TCPAddr 12 | endpoint *HPPServer 13 | pid types.PID 14 | } 15 | 16 | // Endpoint returns the server the client is connecting to 17 | func (c *HPPClient) Endpoint() EndpointInterface { 18 | return c.endpoint 19 | } 20 | 21 | // Address returns the clients address as a net.Addr 22 | func (c *HPPClient) Address() net.Addr { 23 | return c.address 24 | } 25 | 26 | // PID returns the clients NEX PID 27 | func (c *HPPClient) PID() types.PID { 28 | return c.pid 29 | } 30 | 31 | // SetPID sets the clients NEX PID 32 | func (c *HPPClient) SetPID(pid types.PID) { 33 | c.pid = pid 34 | } 35 | 36 | // NewHPPClient creates and returns a new Client using the provided IP address and server 37 | func NewHPPClient(address *net.TCPAddr, server *HPPServer) *HPPClient { 38 | return &HPPClient{ 39 | address: address, 40 | endpoint: server, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /types/rv_type.go: -------------------------------------------------------------------------------- 1 | // Package types provides types used in Quazal Rendez-Vous/NEX 2 | package types 3 | 4 | // RVType represents a Quazal Rendez-Vous/NEX type. 5 | // This includes primitives and custom types. 6 | type RVType interface { 7 | WriteTo(writable Writable) // Writes the type data to the given Writable stream 8 | Copy() RVType // Returns a non-pointer copy of the type data. Complex types are deeply copied 9 | CopyRef() RVTypePtr // Returns a pointer to a copy of the type data. Complex types are deeply copied. Useful for obtaining a pointer without reflection, though limited to copies 10 | Equals(other RVType) bool // Checks if the input type is strictly equal to the current type 11 | } 12 | 13 | // RVTypePtr represents a pointer to an RVType. 14 | // Used to separate pointer receivers for easier type checking. 15 | type RVTypePtr interface { 16 | RVType 17 | ExtractFrom(readable Readable) error // Reads the type data to the given Readable stream 18 | Deref() RVType // Returns the raw type data from a pointer. Useful for ensuring you have raw data without reflection 19 | } 20 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import "fmt" 4 | 5 | // Error is a custom error type implementing the error interface. 6 | type Error struct { 7 | ResultCode uint32 // * NEX result code. See result_codes.go for details 8 | Message string // * The error base message 9 | Packet PacketInterface // * The packet which caused the error. May not always be present 10 | } 11 | 12 | // Error satisfies the error interface and prints the underlying error 13 | func (e Error) Error() string { 14 | resultCode := e.ResultCode 15 | 16 | if int(resultCode)&errorMask != 0 { 17 | // * Result codes are stored without the MSB set 18 | resultCode = resultCode & ^uint32(errorMask) 19 | } 20 | 21 | return fmt.Sprintf("[%s] %s", ResultCodeToName(resultCode), e.Message) 22 | } 23 | 24 | // NewError returns a new NEX error with a RDV result code 25 | func NewError(resultCode uint32, message string) *Error { 26 | if int(resultCode)&errorMask == 0 { 27 | // * Set the MSB to mark the result as an error 28 | resultCode = uint32(int(resultCode) | errorMask) 29 | } 30 | 31 | return &Error{ 32 | ResultCode: resultCode, 33 | Message: message, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /virtual_port.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import "github.com/PretendoNetwork/nex-go/v2/constants" 4 | 5 | // VirtualPort in an implementation of rdv::VirtualPort. 6 | // PRUDP will reuse a single physical socket connection for many virtual PRUDP connections. 7 | // VirtualPorts are a byte which represents a stream for a virtual PRUDP connection. 8 | // This byte is two 4-bit fields. The upper 4 bits are the stream type, the lower 4 bits 9 | // are the stream ID. The client starts with stream ID 15, decrementing by one with each new 10 | // virtual connection. 11 | type VirtualPort byte 12 | 13 | // SetStreamType sets the VirtualPort stream type 14 | func (vp *VirtualPort) SetStreamType(streamType constants.StreamType) { 15 | *vp = VirtualPort((byte(*vp) & 0x0F) | (byte(streamType) << 4)) 16 | } 17 | 18 | // StreamType returns the VirtualPort stream type 19 | func (vp VirtualPort) StreamType() constants.StreamType { 20 | return constants.StreamType(vp >> 4) 21 | } 22 | 23 | // SetStreamID sets the VirtualPort stream ID 24 | func (vp *VirtualPort) SetStreamID(streamID uint8) { 25 | *vp = VirtualPort((byte(*vp) & 0xF0) | (streamID & 0x0F)) 26 | } 27 | 28 | // StreamID returns the VirtualPort stream ID 29 | func (vp VirtualPort) StreamID() uint8 { 30 | return uint8(vp & 0xF) 31 | } 32 | -------------------------------------------------------------------------------- /test/generate_ticket.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | 6 | "github.com/PretendoNetwork/nex-go/v2" 7 | "github.com/PretendoNetwork/nex-go/v2/types" 8 | ) 9 | 10 | func generateTicket(source *nex.Account, target *nex.Account, sessionKeyLength int) []byte { 11 | sourceKey := nex.DeriveKerberosKey(source.PID, []byte(source.Password)) 12 | targetKey := nex.DeriveKerberosKey(target.PID, []byte(target.Password)) 13 | sessionKey := make([]byte, sessionKeyLength) 14 | 15 | _, err := rand.Read(sessionKey) 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | ticketInternalData := nex.NewKerberosTicketInternalData(authServer) 21 | serverTime := types.NewDateTime(0).Now() 22 | 23 | ticketInternalData.Issued = serverTime 24 | ticketInternalData.SourcePID = source.PID 25 | ticketInternalData.SessionKey = sessionKey 26 | 27 | encryptedTicketInternalData, _ := ticketInternalData.Encrypt(targetKey, nex.NewByteStreamOut(authServer.LibraryVersions, authServer.ByteStreamSettings)) 28 | 29 | ticket := nex.NewKerberosTicket() 30 | ticket.SessionKey = sessionKey 31 | ticket.TargetPID = target.PID 32 | ticket.InternalData = types.NewBuffer(encryptedTicketInternalData) 33 | 34 | encryptedTicket, _ := ticket.Encrypt(sourceKey, nex.NewByteStreamOut(authServer.LibraryVersions, authServer.ByteStreamSettings)) 35 | 36 | return encryptedTicket 37 | } 38 | -------------------------------------------------------------------------------- /types/structure.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | // Structure represents a Quazal Rendez-Vous/NEX Structure (custom class) base struct. 9 | type Structure struct { 10 | StructureVersion uint8 `json:"structure_version" db:"structure_version" bson:"structure_version" xml:"StructureVersion"` 11 | } 12 | 13 | // ExtractHeaderFrom extracts the structure header from the given readable 14 | func (s *Structure) ExtractHeaderFrom(readable Readable) error { 15 | if readable.UseStructureHeader() { 16 | version, err := readable.ReadUInt8() 17 | if err != nil { 18 | return fmt.Errorf("Failed to read Structure version. %s", err.Error()) 19 | } 20 | 21 | contentLength, err := readable.ReadUInt32LE() 22 | if err != nil { 23 | return fmt.Errorf("Failed to read Structure content length. %s", err.Error()) 24 | } 25 | 26 | if readable.Remaining() < uint64(contentLength) { 27 | return errors.New("Structure content length longer than data size") 28 | } 29 | 30 | s.StructureVersion = version 31 | } 32 | 33 | return nil 34 | } 35 | 36 | // WriteHeaderTo writes the structure header to the given writable 37 | func (s Structure) WriteHeaderTo(writable Writable, contentLength uint32) { 38 | if writable.UseStructureHeader() { 39 | writable.WriteUInt8(s.StructureVersion) 40 | writable.WriteUInt32LE(contentLength) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /types/writable.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // Writable represents a struct that types can write to 4 | type Writable interface { 5 | StringLengthSize() int // Returns the size of the length field for rdv::String types. Only 2 and 4 are valid 6 | PIDSize() int // Returns the size of the length fields for nn::nex::PID types. Only 4 and 8 are valid 7 | UseStructureHeader() bool // Returns whether or not Structure types should use a header 8 | CopyNew() Writable // Returns a new Writable with the same settings, but an empty buffer 9 | Write(data []byte) // Writes the provided data to the buffer 10 | WriteUInt8(value uint8) // Writes a primitive Go uint8 11 | WriteUInt16LE(value uint16) // Writes a primitive Go uint16 12 | WriteUInt32LE(value uint32) // Writes a primitive Go uint32 13 | WriteUInt64LE(value uint64) // Writes a primitive Go uint64 14 | WriteInt8(value int8) // Writes a primitive Go int8 15 | WriteInt16LE(value int16) // Writes a primitive Go int16 16 | WriteInt32LE(value int32) // Writes a primitive Go int32 17 | WriteInt64LE(value int64) // Writes a primitive Go int64 18 | WriteFloat32LE(value float32) // Writes a primitive Go float32 19 | WriteFloat64LE(value float64) // Writes a primitive Go float64 20 | WriteBool(value bool) // Writes a primitive Go bool 21 | Bytes() []byte // Returns the data written to the buffer 22 | } 23 | -------------------------------------------------------------------------------- /prudp_v0_settings.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import "net" 4 | 5 | // TODO - We can also breakout the decoding/encoding functions here too, but that would require getters and setters for all packet fields 6 | 7 | // PRUDPV0Settings defines settings for how to handle aspects of PRUDPv0 packets 8 | type PRUDPV0Settings struct { 9 | IsQuazalMode bool 10 | EncryptedConnect bool 11 | LegacyConnectionSignature bool 12 | UseEnhancedChecksum bool 13 | ConnectionSignatureCalculator func(packet *PRUDPPacketV0, addr net.Addr) ([]byte, error) 14 | SignatureCalculator func(packet *PRUDPPacketV0, sessionKey, connectionSignature []byte) []byte 15 | DataSignatureCalculator func(packet *PRUDPPacketV0, sessionKey []byte) []byte 16 | ChecksumCalculator func(packet *PRUDPPacketV0, data []byte) uint32 17 | } 18 | 19 | // NewPRUDPV0Settings returns a new PRUDPV0Settings 20 | func NewPRUDPV0Settings() *PRUDPV0Settings { 21 | return &PRUDPV0Settings{ 22 | IsQuazalMode: false, 23 | EncryptedConnect: false, 24 | LegacyConnectionSignature: false, 25 | UseEnhancedChecksum: false, 26 | ConnectionSignatureCalculator: defaultPRUDPv0ConnectionSignature, 27 | SignatureCalculator: defaultPRUDPv0CalculateSignature, 28 | DataSignatureCalculator: defaultPRUDPv0CalculateDataSignature, 29 | ChecksumCalculator: defaultPRUDPv0CalculateChecksum, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PretendoNetwork/nex-go/v2 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/PretendoNetwork/plogger-go v1.0.4 9 | github.com/lib/pq v1.10.9 10 | github.com/lxzan/gws v1.8.8 11 | github.com/prometheus/client_golang v1.23.2 12 | github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e 13 | github.com/stretchr/testify v1.11.1 14 | github.com/superwhiskers/crunch/v3 v3.5.7 15 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 16 | golang.org/x/mod v0.22.0 17 | ) 18 | 19 | require ( 20 | github.com/beorn7/perks v1.0.1 // indirect 21 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/dolthub/maphash v0.1.0 // indirect 24 | github.com/fatih/color v1.18.0 // indirect 25 | github.com/jwalton/go-supportscolor v1.2.0 // indirect 26 | github.com/klauspost/compress v1.18.0 // indirect 27 | github.com/kr/text v0.2.0 // indirect 28 | github.com/mattn/go-colorable v0.1.14 // indirect 29 | github.com/mattn/go-isatty v0.0.20 // indirect 30 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 31 | github.com/pmezard/go-difflib v1.0.0 // indirect 32 | github.com/prometheus/client_model v0.6.2 // indirect 33 | github.com/prometheus/common v0.66.1 // indirect 34 | github.com/prometheus/procfs v0.16.1 // indirect 35 | go.yaml.in/yaml/v2 v2.4.2 // indirect 36 | golang.org/x/sys v0.35.0 // indirect 37 | golang.org/x/term v0.28.0 // indirect 38 | google.golang.org/protobuf v1.36.8 // indirect 39 | gopkg.in/yaml.v3 v3.0.1 // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /packet_dispatch_queue_test.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestReorderPackets(t *testing.T) { 10 | pdq := NewPacketDispatchQueue() 11 | 12 | packet1 := makePacket(2) 13 | packet2 := makePacket(3) 14 | packet3 := makePacket(4) 15 | 16 | pdq.Queue(packet2) 17 | pdq.Queue(packet3) 18 | pdq.Queue(packet1) 19 | 20 | result, ok := pdq.GetNextToDispatch() 21 | assert.True(t, ok) 22 | assert.Equal(t, packet1, result) 23 | pdq.Dispatched(packet1) 24 | 25 | result, ok = pdq.GetNextToDispatch() 26 | assert.True(t, ok) 27 | assert.Equal(t, packet2, result) 28 | pdq.Dispatched(packet2) 29 | 30 | result, ok = pdq.GetNextToDispatch() 31 | assert.True(t, ok) 32 | assert.Equal(t, packet3, result) 33 | pdq.Dispatched(packet3) 34 | 35 | result, ok = pdq.GetNextToDispatch() 36 | assert.False(t, ok) 37 | assert.Nil(t, result) 38 | } 39 | 40 | func TestCallingInLoop(t *testing.T) { 41 | pdq := NewPacketDispatchQueue() 42 | 43 | packet1 := makePacket(2) 44 | packet2 := makePacket(3) 45 | packet3 := makePacket(4) 46 | 47 | pdq.Queue(packet2) 48 | pdq.Queue(packet3) 49 | pdq.Queue(packet1) 50 | 51 | for nextPacket, ok := pdq.GetNextToDispatch(); ok; nextPacket, ok = pdq.GetNextToDispatch() { 52 | pdq.Dispatched(nextPacket) 53 | } 54 | 55 | assert.Equal(t, uint16(5), pdq.nextExpectedSequenceId.Value) 56 | } 57 | 58 | func makePacket(sequenceID uint16) PRUDPPacketInterface { 59 | packet, _ := NewPRUDPPacketV0(nil, nil, nil) 60 | packet.SetSequenceID(sequenceID) 61 | 62 | return packet 63 | } 64 | -------------------------------------------------------------------------------- /account.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import "github.com/PretendoNetwork/nex-go/v2/types" 4 | 5 | // Account represents a game server account. 6 | // 7 | // Game server accounts are separate from other accounts, like Uplay, Nintendo Accounts and NNIDs. 8 | // These exist only on the game server. Account passwords are used as part of the servers Kerberos 9 | // authentication. There are also a collection of non-user, special, accounts. These include a 10 | // guest account, an account which represents the authentication server, and one which represents 11 | // the secure server. See https://nintendo-wiki.pretendo.network/docs/nex/kerberos for more information. 12 | type Account struct { 13 | PID types.PID // * The PID of the account. PIDs are unique IDs per account. NEX PIDs start at 1800000000 and decrement with each new account. 14 | Username string // * The username for the account. For NEX user accounts this is the same as the accounts PID. 15 | Password string // * The password for the account. For NEX accounts this is always 16 characters long using seemingly any ASCII character. 16 | RequiresTokenAuth bool // * If the account requires token authentication. Always false for special accounts or user accounts pre-Switch. 17 | } 18 | 19 | // NewAccount returns a new instance of Account. 20 | // This does not register an account, only creates a new 21 | // struct instance. 22 | func NewAccount(pid types.PID, username, password string, requiresTokenAuth bool) *Account { 23 | return &Account{ 24 | PID: pid, 25 | Username: username, 26 | Password: password, 27 | RequiresTokenAuth: requiresTokenAuth, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sliding_window.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | // SlidingWindow is an implementation of rdv::SlidingWindow. 4 | // Currently this is a stub and does not reflect the interface and usage of rdv:SlidingWindow. 5 | // In the original library this is used to manage sequencing of outgoing packets. 6 | // each virtual connection stream only uses a single SlidingWindow, but starting 7 | // in PRUDPv1 with NEX virtual connections may have multiple reliable substreams and thus multiple SlidingWindows. 8 | type SlidingWindow struct { 9 | sequenceIDCounter *Counter[uint16] 10 | streamSettings *StreamSettings 11 | TimeoutManager *TimeoutManager 12 | } 13 | 14 | // SetCipherKey sets the reliable substreams RC4 cipher keys 15 | func (sw *SlidingWindow) SetCipherKey(key []byte) { 16 | sw.streamSettings.EncryptionAlgorithm.SetKey(key) 17 | } 18 | 19 | // NextOutgoingSequenceID sets the reliable substreams RC4 cipher keys 20 | func (sw *SlidingWindow) NextOutgoingSequenceID() uint16 { 21 | return sw.sequenceIDCounter.Next() 22 | } 23 | 24 | // Decrypt decrypts the provided data with the substreams decipher 25 | func (sw *SlidingWindow) Decrypt(data []byte) ([]byte, error) { 26 | return sw.streamSettings.EncryptionAlgorithm.Decrypt(data) 27 | } 28 | 29 | // Encrypt encrypts the provided data with the substreams cipher 30 | func (sw *SlidingWindow) Encrypt(data []byte) ([]byte, error) { 31 | return sw.streamSettings.EncryptionAlgorithm.Encrypt(data) 32 | } 33 | 34 | // NewSlidingWindow initializes a new SlidingWindow with a starting counter value. 35 | func NewSlidingWindow() *SlidingWindow { 36 | sw := &SlidingWindow{ 37 | sequenceIDCounter: NewCounter[uint16](0), 38 | TimeoutManager: NewTimeoutManager(), 39 | } 40 | 41 | return sw 42 | } 43 | -------------------------------------------------------------------------------- /compression/lzo.go: -------------------------------------------------------------------------------- 1 | package compression 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/rasky/go-lzo" 8 | ) 9 | 10 | // TODO - Untested. I think this works. Maybe. Verify and remove this comment 11 | 12 | // LZO implements packet payload compression using LZO 13 | type LZO struct{} 14 | 15 | // Compress compresses the payload using LZO 16 | func (l *LZO) Compress(payload []byte) ([]byte, error) { 17 | compressed := lzo.Compress1X(payload) 18 | 19 | compressionRatio := len(payload)/len(compressed) + 1 20 | 21 | result := make([]byte, len(compressed)+1) 22 | 23 | result[0] = byte(compressionRatio) 24 | 25 | copy(result[1:], compressed) 26 | 27 | return result, nil 28 | } 29 | 30 | // Decompress decompresses the payload using LZO 31 | func (l *LZO) Decompress(payload []byte) ([]byte, error) { 32 | compressionRatio := payload[0] 33 | compressed := payload[1:] 34 | 35 | if compressionRatio == 0 { 36 | // * Compression ratio of 0 means no compression 37 | return compressed, nil 38 | } 39 | 40 | reader := bytes.NewReader(compressed) 41 | decompressed, err := lzo.Decompress1X(reader, len(compressed), 0) 42 | if err != nil { 43 | return []byte{}, err 44 | } 45 | 46 | ratioCheck := len(decompressed)/len(compressed) + 1 47 | 48 | if ratioCheck != int(compressionRatio) { 49 | return []byte{}, fmt.Errorf("Failed to decompress payload. Got bad ratio. Expected %d, got %d", compressionRatio, ratioCheck) 50 | } 51 | 52 | return decompressed, nil 53 | } 54 | 55 | // Copy returns a copy of the algorithm 56 | func (l *LZO) Copy() Algorithm { 57 | return NewLZOCompression() 58 | } 59 | 60 | // NewLZOCompression returns a new instance of the LZO compression 61 | func NewLZOCompression() *LZO { 62 | return &LZO{} 63 | } 64 | -------------------------------------------------------------------------------- /test/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/PretendoNetwork/nex-go/v2" 7 | "github.com/PretendoNetwork/nex-go/v2/types" 8 | ) 9 | 10 | var wg sync.WaitGroup 11 | 12 | var authenticationServerAccount *nex.Account 13 | var secureServerAccount *nex.Account 14 | var testUserAccount *nex.Account 15 | 16 | func accountDetailsByPID(pid types.PID) (*nex.Account, *nex.Error) { 17 | if pid.Equals(authenticationServerAccount.PID) { 18 | return authenticationServerAccount, nil 19 | } 20 | 21 | if pid.Equals(secureServerAccount.PID) { 22 | return secureServerAccount, nil 23 | } 24 | 25 | if pid.Equals(testUserAccount.PID) { 26 | return testUserAccount, nil 27 | } 28 | 29 | return nil, nex.NewError(nex.ResultCodes.RendezVous.InvalidPID, "Invalid PID") 30 | } 31 | 32 | func accountDetailsByUsername(username string) (*nex.Account, *nex.Error) { 33 | if username == authenticationServerAccount.Username { 34 | return authenticationServerAccount, nil 35 | } 36 | 37 | if username == secureServerAccount.Username { 38 | return secureServerAccount, nil 39 | } 40 | 41 | if username == testUserAccount.Username { 42 | return testUserAccount, nil 43 | } 44 | 45 | return nil, nex.NewError(nex.ResultCodes.RendezVous.InvalidPID, "Invalid username") 46 | } 47 | 48 | func main() { 49 | authenticationServerAccount = nex.NewAccount(types.NewPID(1), "Quazal Authentication", "authpassword", false) 50 | secureServerAccount = nex.NewAccount(types.NewPID(2), "Quazal Rendez-Vous", "securepassword", false) 51 | testUserAccount = nex.NewAccount(types.NewPID(1800000000), "1800000000", "nexuserpassword", false) 52 | 53 | wg.Add(3) 54 | 55 | go startAuthenticationServer() 56 | go startSecureServer() 57 | go startHPPServer() 58 | 59 | wg.Wait() 60 | } 61 | -------------------------------------------------------------------------------- /constants/stream_type.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // TODO - Should this be moved to the types module? 4 | 5 | // StreamType is an implementation of the rdv::Stream::Type enum. 6 | // 7 | // StreamType is used to create VirtualPorts used in PRUDP virtual 8 | // connections. Each stream may be one of these types, and each stream 9 | // has it's own state. 10 | type StreamType uint8 11 | 12 | // EnumIndex returns the StreamType enum index as a uint8 13 | func (st StreamType) EnumIndex() uint8 { 14 | return uint8(st) 15 | } 16 | 17 | const ( 18 | // StreamTypeDO represents the DO PRUDP virtual connection stream type 19 | StreamTypeDO StreamType = iota + 1 20 | 21 | // StreamTypeRV represents the RV PRUDP virtual connection stream type 22 | StreamTypeRV 23 | 24 | // StreamTypeOldRVSec represents the OldRVSec PRUDP virtual connection stream type 25 | StreamTypeOldRVSec 26 | 27 | // StreamTypeSBMGMT represents the SBMGMT PRUDP virtual connection stream type 28 | StreamTypeSBMGMT 29 | 30 | // StreamTypeNAT represents the NAT PRUDP virtual connection stream type 31 | StreamTypeNAT 32 | 33 | // StreamTypeSessionDiscovery represents the SessionDiscovery PRUDP virtual connection stream type 34 | StreamTypeSessionDiscovery 35 | 36 | // StreamTypeNATEcho represents the NATEcho PRUDP virtual connection stream type 37 | StreamTypeNATEcho 38 | 39 | // StreamTypeRouting represents the Routing PRUDP virtual connection stream type 40 | StreamTypeRouting 41 | 42 | // StreamTypeGame represents the Game PRUDP virtual connection stream type 43 | StreamTypeGame 44 | 45 | // StreamTypeRVSecure represents the RVSecure PRUDP virtual connection stream type 46 | StreamTypeRVSecure 47 | 48 | // StreamTypeRelay represents the Relay PRUDP virtual connection stream type 49 | StreamTypeRelay 50 | ) 51 | -------------------------------------------------------------------------------- /prudp_packet_interface.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/PretendoNetwork/nex-go/v2/constants" 8 | ) 9 | 10 | // PRUDPPacketInterface defines all the methods a PRUDP packet should have 11 | type PRUDPPacketInterface interface { 12 | Copy() PRUDPPacketInterface 13 | Version() int 14 | Bytes() []byte 15 | SetSender(sender ConnectionInterface) 16 | Sender() ConnectionInterface 17 | Flags() uint16 18 | HasFlag(flag uint16) bool 19 | AddFlag(flag uint16) 20 | SetType(packetType uint16) 21 | Type() uint16 22 | SetSourceVirtualPortStreamType(streamType constants.StreamType) 23 | SourceVirtualPortStreamType() constants.StreamType 24 | SetSourceVirtualPortStreamID(port uint8) 25 | SourceVirtualPortStreamID() uint8 26 | SetDestinationVirtualPortStreamType(streamType constants.StreamType) 27 | DestinationVirtualPortStreamType() constants.StreamType 28 | SetDestinationVirtualPortStreamID(port uint8) 29 | DestinationVirtualPortStreamID() uint8 30 | SessionID() uint8 31 | SetSessionID(sessionID uint8) 32 | SubstreamID() uint8 33 | SetSubstreamID(substreamID uint8) 34 | SequenceID() uint16 35 | SetSequenceID(sequenceID uint16) 36 | Payload() []byte 37 | SetPayload(payload []byte) 38 | RMCMessage() *RMCMessage 39 | SetRMCMessage(message *RMCMessage) 40 | SendCount() uint32 41 | incrementSendCount() 42 | SentAt() time.Time 43 | setSentAt(time time.Time) 44 | getTimeout() *Timeout 45 | setTimeout(timeout *Timeout) 46 | decode() error 47 | SetSignature(signature []byte) 48 | CalculateConnectionSignature(addr net.Addr) ([]byte, error) 49 | CalculateSignature(sessionKey, connectionSignature []byte) []byte 50 | decryptPayload() []byte 51 | GetConnectionSignature() []byte 52 | SetConnectionSignature(connectionSignature []byte) 53 | getFragmentID() uint8 54 | setFragmentID(fragmentID uint8) 55 | processUnreliableCrypto() []byte 56 | } 57 | -------------------------------------------------------------------------------- /packet_dispatch_queue.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | // PacketDispatchQueue is an implementation of rdv::PacketDispatchQueue. 4 | // PacketDispatchQueue is used to sequence incoming packets. 5 | // In the original library each virtual connection stream only uses a single PacketDispatchQueue, but starting 6 | // in PRUDPv1 NEX virtual connections may have multiple reliable substreams and thus multiple PacketDispatchQueues. 7 | type PacketDispatchQueue struct { 8 | queue map[uint16]PRUDPPacketInterface 9 | nextExpectedSequenceId *Counter[uint16] 10 | } 11 | 12 | // Queue adds a packet to the queue to be dispatched 13 | func (pdq *PacketDispatchQueue) Queue(packet PRUDPPacketInterface) { 14 | pdq.queue[packet.SequenceID()] = packet 15 | } 16 | 17 | // GetNextToDispatch returns the next packet to be dispatched, nil if there are no packets 18 | // and a boolean indicating whether anything was returned. 19 | func (pdq *PacketDispatchQueue) GetNextToDispatch() (PRUDPPacketInterface, bool) { 20 | if packet, ok := pdq.queue[pdq.nextExpectedSequenceId.Value]; ok { 21 | return packet, true 22 | } 23 | 24 | return nil, false 25 | } 26 | 27 | // Dispatched removes a packet from the queue to be dispatched. 28 | func (pdq *PacketDispatchQueue) Dispatched(packet PRUDPPacketInterface) { 29 | pdq.nextExpectedSequenceId.Next() 30 | delete(pdq.queue, packet.SequenceID()) 31 | } 32 | 33 | // Purge clears the queue of all pending packets. 34 | func (pdq *PacketDispatchQueue) Purge() { 35 | clear(pdq.queue) 36 | } 37 | 38 | // NewPacketDispatchQueue initializes a new PacketDispatchQueue with a starting counter value. 39 | func NewPacketDispatchQueue() *PacketDispatchQueue { 40 | return &PacketDispatchQueue{ 41 | queue: make(map[uint16]PRUDPPacketInterface), 42 | nextExpectedSequenceId: NewCounter[uint16](2), // * First DATA packet from a client will always be 2 as the CONNECT packet is assigned 1 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /rtt.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import ( 4 | "math" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | const ( 10 | alpha float64 = 1.0 / 8.0 11 | beta float64 = 1.0 / 4.0 12 | k float64 = 4.0 13 | ) 14 | 15 | // RTT is an implementation of rdv::RTT. 16 | // Used to calculate the average round trip time of reliable packets 17 | type RTT struct { 18 | sync.Mutex 19 | lastRTT float64 20 | average float64 21 | variance float64 22 | initialized bool 23 | } 24 | 25 | // Adjust updates the average RTT with the new value 26 | func (rtt *RTT) Adjust(next time.Duration) { 27 | // * This calculation comes from the RFC6298 which defines RTT calculation for TCP packets 28 | rtt.Lock() 29 | if rtt.initialized { 30 | rtt.variance = (1.0-beta)*rtt.variance + beta*math.Abs(rtt.variance-float64(next)) 31 | rtt.average = (1.0-alpha)*rtt.average + alpha*float64(next) 32 | } else { 33 | rtt.lastRTT = float64(next) 34 | rtt.variance = float64(next) / 2 35 | rtt.average = float64(next) + k*rtt.variance 36 | rtt.initialized = true 37 | } 38 | rtt.Unlock() 39 | } 40 | 41 | // GetRTTSmoothedAvg returns the smoothed average of this RTT, it is used in calls to the custom 42 | // RTO calculation function set on `PRUDPEndpoint::SetCalcRetransmissionTimeoutCallback` 43 | func (rtt *RTT) GetRTTSmoothedAvg() float64 { 44 | return rtt.average / 16 45 | } 46 | 47 | // GetRTTSmoothedDev returns the smoothed standard deviation of this RTT, it is used in calls to the custom 48 | // RTO calculation function set on `PRUDPEndpoint::SetCalcRetransmissionTimeoutCallback` 49 | func (rtt *RTT) GetRTTSmoothedDev() float64 { 50 | return rtt.variance / 8 51 | } 52 | 53 | // Initialized returns a bool indicating whether this RTT has been initialized 54 | func (rtt *RTT) Initialized() bool { 55 | return rtt.initialized 56 | } 57 | 58 | // GetRTO returns the current average 59 | func (rtt *RTT) Average() time.Duration { 60 | return time.Duration(rtt.average) 61 | } 62 | 63 | // NewRTT returns a new RTT based on the first value 64 | func NewRTT() *RTT { 65 | return &RTT{} 66 | } 67 | -------------------------------------------------------------------------------- /types/bool.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | ) 7 | 8 | // Bool is a type alias for the Go basic type bool for use as an RVType 9 | type Bool bool 10 | 11 | // WriteTo writes the Bool to the given writable 12 | func (b Bool) WriteTo(writable Writable) { 13 | writable.WriteBool(bool(b)) 14 | } 15 | 16 | // ExtractFrom extracts the Bool value from the given readable 17 | func (b *Bool) ExtractFrom(readable Readable) error { 18 | value, err := readable.ReadBool() 19 | if err != nil { 20 | return err 21 | } 22 | 23 | *b = Bool(value) 24 | return nil 25 | } 26 | 27 | // Copy returns a pointer to a copy of the Bool. Requires type assertion when used 28 | func (b Bool) Copy() RVType { 29 | return NewBool(bool(b)) 30 | } 31 | 32 | // Equals checks if the input is equal in value to the current instance 33 | func (b Bool) Equals(o RVType) bool { 34 | other, ok := o.(Bool) 35 | if !ok { 36 | return false 37 | } 38 | 39 | return b == other 40 | } 41 | 42 | // CopyRef copies the current value of the Bool 43 | // and returns a pointer to the new copy 44 | func (b Bool) CopyRef() RVTypePtr { 45 | copied := b.Copy().(Bool) 46 | return &copied 47 | } 48 | 49 | // Deref takes a pointer to the Bool 50 | // and dereferences it to the raw value. 51 | // Only useful when working with an instance of RVTypePtr 52 | func (b *Bool) Deref() RVType { 53 | return *b 54 | } 55 | 56 | // String returns a string representation of the Bool 57 | func (b Bool) String() string { 58 | return fmt.Sprintf("%t", b) 59 | } 60 | 61 | // Scan implements sql.Scanner for database/sql 62 | func (b *Bool) Scan(value any) error { 63 | if value == nil { 64 | *b = Bool(false) 65 | return nil 66 | } 67 | 68 | switch v := value.(type) { 69 | case bool: 70 | *b = Bool(v) 71 | case string: 72 | *b = Bool(v == "t" || v == "true") 73 | default: 74 | return fmt.Errorf("cannot scan %T into Bool", value) 75 | } 76 | 77 | return nil 78 | } 79 | 80 | // Value implements driver.Valuer for database/sql 81 | func (b Bool) Value() (driver.Value, error) { 82 | return bool(b), nil 83 | } 84 | 85 | // NewBool returns a new Bool 86 | func NewBool(input bool) Bool { 87 | b := Bool(input) 88 | return b 89 | } 90 | -------------------------------------------------------------------------------- /compression/zlib.go: -------------------------------------------------------------------------------- 1 | package compression 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "fmt" 7 | ) 8 | 9 | // Zlib implements packet payload compression using zlib 10 | type Zlib struct{} 11 | 12 | // Compress compresses the payload using zlib 13 | func (z *Zlib) Compress(payload []byte) ([]byte, error) { 14 | compressed := bytes.Buffer{} 15 | 16 | zlibWriter := zlib.NewWriter(&compressed) 17 | 18 | _, err := zlibWriter.Write(payload) 19 | if err != nil { 20 | return []byte{}, err 21 | } 22 | 23 | err = zlibWriter.Close() 24 | if err != nil { 25 | return []byte{}, err 26 | } 27 | 28 | compressedBytes := compressed.Bytes() 29 | 30 | compressionRatio := len(payload)/len(compressedBytes) + 1 31 | 32 | result := make([]byte, len(compressedBytes)+1) 33 | 34 | result[0] = byte(compressionRatio) 35 | 36 | copy(result[1:], compressedBytes) 37 | 38 | return result, nil 39 | } 40 | 41 | // Decompress decompresses the payload using zlib 42 | func (z *Zlib) Decompress(payload []byte) ([]byte, error) { 43 | compressionRatio := payload[0] 44 | compressed := payload[1:] 45 | 46 | if compressionRatio == 0 { 47 | // * Compression ratio of 0 means no compression 48 | return compressed, nil 49 | } 50 | 51 | reader := bytes.NewReader(compressed) 52 | decompressed := bytes.Buffer{} 53 | 54 | zlibReader, err := zlib.NewReader(reader) 55 | if err != nil { 56 | return []byte{}, err 57 | } 58 | 59 | _, err = decompressed.ReadFrom(zlibReader) 60 | if err != nil { 61 | return []byte{}, err 62 | } 63 | 64 | err = zlibReader.Close() 65 | if err != nil { 66 | return []byte{}, err 67 | } 68 | 69 | decompressedBytes := decompressed.Bytes() 70 | 71 | ratioCheck := len(decompressedBytes)/len(compressed) + 1 72 | 73 | if ratioCheck != int(compressionRatio) { 74 | return []byte{}, fmt.Errorf("Failed to decompress payload. Got bad ratio. Expected %d, got %d", compressionRatio, ratioCheck) 75 | } 76 | 77 | return decompressedBytes, nil 78 | } 79 | 80 | // Copy returns a copy of the algorithm 81 | func (z *Zlib) Copy() Algorithm { 82 | return NewZlibCompression() 83 | } 84 | 85 | // NewZlibCompression returns a new instance of the Zlib compression 86 | func NewZlibCompression() *Zlib { 87 | return &Zlib{} 88 | } 89 | -------------------------------------------------------------------------------- /types/int8.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | // Int8 is a type alias for the Go basic type int8 for use as an RVType 10 | type Int8 int8 11 | 12 | // WriteTo writes the Int8 to the given writable 13 | func (i8 Int8) WriteTo(writable Writable) { 14 | writable.WriteInt8(int8(i8)) 15 | } 16 | 17 | // ExtractFrom extracts the Int8 value from the given readable 18 | func (i8 *Int8) ExtractFrom(readable Readable) error { 19 | value, err := readable.ReadInt8() 20 | if err != nil { 21 | return err 22 | } 23 | 24 | *i8 = Int8(value) 25 | return nil 26 | } 27 | 28 | // Copy returns a pointer to a copy of the Int8. Requires type assertion when used 29 | func (i8 Int8) Copy() RVType { 30 | return NewInt8(int8(i8)) 31 | } 32 | 33 | // Equals checks if the input is equal in value to the current instance 34 | func (i8 Int8) Equals(o RVType) bool { 35 | other, ok := o.(Int8) 36 | if !ok { 37 | return false 38 | } 39 | 40 | return i8 == other 41 | } 42 | 43 | // CopyRef copies the current value of the Int8 44 | // and returns a pointer to the new copy 45 | func (i8 Int8) CopyRef() RVTypePtr { 46 | copied := i8.Copy().(Int8) 47 | return &copied 48 | } 49 | 50 | // Deref takes a pointer to the Int8 51 | // and dereferences it to the raw value. 52 | // Only useful when working with an instance of RVTypePtr 53 | func (i8 *Int8) Deref() RVType { 54 | return *i8 55 | } 56 | 57 | // String returns a string representation of the Int8 58 | func (i8 Int8) String() string { 59 | return fmt.Sprintf("%d", i8) 60 | } 61 | 62 | // Scan implements sql.Scanner for database/sql 63 | func (i8 *Int8) Scan(value any) error { 64 | if value == nil { 65 | *i8 = Int8(0) 66 | return nil 67 | } 68 | 69 | switch v := value.(type) { 70 | case int64: 71 | *i8 = Int8(v) 72 | case string: 73 | parsed, err := strconv.ParseInt(v, 10, 8) 74 | if err != nil { 75 | return fmt.Errorf("cannot parse string %q into Int8: %w", v, err) 76 | } 77 | *i8 = Int8(parsed) 78 | default: 79 | return fmt.Errorf("cannot scan %T into Int8", value) 80 | } 81 | 82 | return nil 83 | } 84 | 85 | // Value implements driver.Valuer for database/sql 86 | func (i8 Int8) Value() (driver.Value, error) { 87 | return int64(i8), nil 88 | } 89 | 90 | // NewInt8 returns a new Int8 91 | func NewInt8(input int8) Int8 { 92 | i8 := Int8(input) 93 | return i8 94 | } 95 | -------------------------------------------------------------------------------- /types/uint8.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | // UInt8 is a type alias for the Go basic type uint8 for use as an RVType 10 | type UInt8 uint8 11 | 12 | // WriteTo writes the UInt8 to the given writable 13 | func (u8 UInt8) WriteTo(writable Writable) { 14 | writable.WriteUInt8(uint8(u8)) 15 | } 16 | 17 | // ExtractFrom extracts the UInt8 value from the given readable 18 | func (u8 *UInt8) ExtractFrom(readable Readable) error { 19 | value, err := readable.ReadUInt8() 20 | if err != nil { 21 | return err 22 | } 23 | 24 | *u8 = UInt8(value) 25 | return nil 26 | } 27 | 28 | // Copy returns a pointer to a copy of the UInt8. Requires type assertion when used 29 | func (u8 UInt8) Copy() RVType { 30 | return NewUInt8(uint8(u8)) 31 | } 32 | 33 | // Equals checks if the input is equal in value to the current instance 34 | func (u8 UInt8) Equals(o RVType) bool { 35 | other, ok := o.(UInt8) 36 | if !ok { 37 | return false 38 | } 39 | 40 | return u8 == other 41 | } 42 | 43 | // CopyRef copies the current value of the UInt8 44 | // and returns a pointer to the new copy 45 | func (u8 UInt8) CopyRef() RVTypePtr { 46 | copied := u8.Copy().(UInt8) 47 | return &copied 48 | } 49 | 50 | // Deref takes a pointer to the UInt8 51 | // and dereferences it to the raw value. 52 | // Only useful when working with an instance of RVTypePtr 53 | func (u8 *UInt8) Deref() RVType { 54 | return *u8 55 | } 56 | 57 | // String returns a string representation of the UInt8 58 | func (u8 UInt8) String() string { 59 | return fmt.Sprintf("%d", u8) 60 | } 61 | 62 | // Scan implements sql.Scanner for database/sql 63 | func (u8 *UInt8) Scan(value any) error { 64 | if value == nil { 65 | *u8 = UInt8(0) 66 | return nil 67 | } 68 | 69 | switch v := value.(type) { 70 | case int64: 71 | *u8 = UInt8(v) 72 | case string: 73 | parsed, err := strconv.ParseUint(v, 10, 8) 74 | if err != nil { 75 | return fmt.Errorf("cannot parse string %q into UInt8: %w", v, err) 76 | } 77 | *u8 = UInt8(parsed) 78 | default: 79 | return fmt.Errorf("cannot scan %T into UInt8", value) 80 | } 81 | 82 | return nil 83 | } 84 | 85 | // Value implements driver.Valuer for database/sql 86 | func (u8 UInt8) Value() (driver.Value, error) { 87 | return int64(u8), nil 88 | } 89 | 90 | // NewUInt8 returns a new UInt8 91 | func NewUInt8(input uint8) UInt8 { 92 | u8 := UInt8(input) 93 | return u8 94 | } 95 | -------------------------------------------------------------------------------- /types/float.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | // Float is a type alias for the Go basic type float32 for use as an RVType 10 | type Float float32 11 | 12 | // WriteTo writes the Float to the given writable 13 | func (f Float) WriteTo(writable Writable) { 14 | writable.WriteFloat32LE(float32(f)) 15 | } 16 | 17 | // ExtractFrom extracts the Float value from the given readable 18 | func (f *Float) ExtractFrom(readable Readable) error { 19 | value, err := readable.ReadFloat32LE() 20 | if err != nil { 21 | return err 22 | } 23 | 24 | *f = Float(value) 25 | return nil 26 | } 27 | 28 | // Copy returns a pointer to a copy of the Float. Requires type assertion when used 29 | func (f Float) Copy() RVType { 30 | return NewFloat(float32(f)) 31 | } 32 | 33 | // Equals checks if the input is equal in value to the current instance 34 | func (f Float) Equals(o RVType) bool { 35 | other, ok := o.(Float) 36 | if !ok { 37 | return false 38 | } 39 | 40 | return f == other 41 | } 42 | 43 | // CopyRef copies the current value of the Float 44 | // and returns a pointer to the new copy 45 | func (f Float) CopyRef() RVTypePtr { 46 | copied := f.Copy().(Float) 47 | return &copied 48 | } 49 | 50 | // Deref takes a pointer to the Float 51 | // and dereferences it to the raw value. 52 | // Only useful when working with an instance of RVTypePtr 53 | func (f *Float) Deref() RVType { 54 | return *f 55 | } 56 | 57 | // String returns a string representation of the Float 58 | func (f Float) String() string { 59 | return fmt.Sprintf("%f", f) 60 | } 61 | 62 | // Scan implements sql.Scanner for database/sql 63 | func (f *Float) Scan(value any) error { 64 | if value == nil { 65 | *f = Float(0.0) 66 | return nil 67 | } 68 | 69 | switch v := value.(type) { 70 | case float64: 71 | *f = Float(value.(float64)) 72 | case string: 73 | parsed, err := strconv.ParseFloat(v, 32) 74 | if err != nil { 75 | return fmt.Errorf("cannot parse string %q into Float: %w", v, err) 76 | } 77 | *f = Float(parsed) 78 | default: 79 | return fmt.Errorf("cannot scan %T into Float", value) 80 | } 81 | 82 | return nil 83 | } 84 | 85 | // Value implements driver.Valuer for database/sql 86 | func (f Float) Value() (driver.Value, error) { 87 | return float64(f), nil 88 | } 89 | 90 | // NewFloat returns a new Float 91 | func NewFloat(input float32) Float { 92 | f := Float(input) 93 | return f 94 | } 95 | -------------------------------------------------------------------------------- /types/int16.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | // Int16 is a type alias for the Go basic type int16 for use as an RVType 10 | type Int16 int16 11 | 12 | // WriteTo writes the Int16 to the given writable 13 | func (i16 Int16) WriteTo(writable Writable) { 14 | writable.WriteInt16LE(int16(i16)) 15 | } 16 | 17 | // ExtractFrom extracts the Int16 value from the given readable 18 | func (i16 *Int16) ExtractFrom(readable Readable) error { 19 | value, err := readable.ReadInt16LE() 20 | if err != nil { 21 | return err 22 | } 23 | 24 | *i16 = Int16(value) 25 | return nil 26 | } 27 | 28 | // Copy returns a pointer to a copy of the Int16. Requires type assertion when used 29 | func (i16 Int16) Copy() RVType { 30 | return NewInt16(int16(i16)) 31 | } 32 | 33 | // Equals checks if the input is equal in value to the current instance 34 | func (i16 Int16) Equals(o RVType) bool { 35 | other, ok := o.(Int16) 36 | if !ok { 37 | return false 38 | } 39 | 40 | return i16 == other 41 | } 42 | 43 | // CopyRef copies the current value of the Int16 44 | // and returns a pointer to the new copy 45 | func (i16 Int16) CopyRef() RVTypePtr { 46 | copied := i16.Copy().(Int16) 47 | return &copied 48 | } 49 | 50 | // Deref takes a pointer to the Int16 51 | // and dereferences it to the raw value. 52 | // Only useful when working with an instance of RVTypePtr 53 | func (i16 *Int16) Deref() RVType { 54 | return *i16 55 | } 56 | 57 | // String returns a string representation of the Int16 58 | func (i16 Int16) String() string { 59 | return fmt.Sprintf("%d", i16) 60 | } 61 | 62 | // Scan implements sql.Scanner for database/sql 63 | func (i16 *Int16) Scan(value any) error { 64 | if value == nil { 65 | *i16 = Int16(0) 66 | return nil 67 | } 68 | 69 | switch v := value.(type) { 70 | case int64: 71 | *i16 = Int16(v) 72 | case string: 73 | parsed, err := strconv.ParseInt(v, 10, 16) 74 | if err != nil { 75 | return fmt.Errorf("cannot parse string %q into Int16: %w", v, err) 76 | } 77 | *i16 = Int16(parsed) 78 | default: 79 | return fmt.Errorf("cannot scan %T into Int16", value) 80 | } 81 | 82 | return nil 83 | } 84 | 85 | // Value implements driver.Valuer for database/sql 86 | func (i16 Int16) Value() (driver.Value, error) { 87 | return int64(i16), nil 88 | } 89 | 90 | // NewInt16 returns a new Int16 91 | func NewInt16(input int16) Int16 { 92 | i16 := Int16(input) 93 | return i16 94 | } 95 | -------------------------------------------------------------------------------- /types/int32.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | // Int32 is a type alias for the Go basic type int32 for use as an RVType 10 | type Int32 int32 11 | 12 | // WriteTo writes the Int32 to the given writable 13 | func (i32 Int32) WriteTo(writable Writable) { 14 | writable.WriteInt32LE(int32(i32)) 15 | } 16 | 17 | // ExtractFrom extracts the Int32 value from the given readable 18 | func (i32 *Int32) ExtractFrom(readable Readable) error { 19 | value, err := readable.ReadInt32LE() 20 | if err != nil { 21 | return err 22 | } 23 | 24 | *i32 = Int32(value) 25 | return nil 26 | } 27 | 28 | // Copy returns a pointer to a copy of the Int32. Requires type assertion when used 29 | func (i32 Int32) Copy() RVType { 30 | return NewInt32(int32(i32)) 31 | } 32 | 33 | // Equals checks if the input is equal in value to the current instance 34 | func (i32 Int32) Equals(o RVType) bool { 35 | other, ok := o.(Int32) 36 | if !ok { 37 | return false 38 | } 39 | 40 | return i32 == other 41 | } 42 | 43 | // CopyRef copies the current value of the Int32 44 | // and returns a pointer to the new copy 45 | func (i32 Int32) CopyRef() RVTypePtr { 46 | copied := i32.Copy().(Int32) 47 | return &copied 48 | } 49 | 50 | // Deref takes a pointer to the Int32 51 | // and dereferences it to the raw value. 52 | // Only useful when working with an instance of RVTypePtr 53 | func (i32 *Int32) Deref() RVType { 54 | return *i32 55 | } 56 | 57 | // String returns a string representation of the Int32 58 | func (i32 Int32) String() string { 59 | return fmt.Sprintf("%d", i32) 60 | } 61 | 62 | // Scan implements sql.Scanner for database/sql 63 | func (i32 *Int32) Scan(value any) error { 64 | if value == nil { 65 | *i32 = Int32(0) 66 | return nil 67 | } 68 | 69 | switch v := value.(type) { 70 | case int64: 71 | *i32 = Int32(v) 72 | case string: 73 | parsed, err := strconv.ParseInt(v, 10, 32) 74 | if err != nil { 75 | return fmt.Errorf("cannot parse string %q into Int32: %w", v, err) 76 | } 77 | *i32 = Int32(parsed) 78 | default: 79 | return fmt.Errorf("cannot scan %T into Int32", value) 80 | } 81 | 82 | return nil 83 | } 84 | 85 | // Value implements driver.Valuer for database/sql 86 | func (i32 Int32) Value() (driver.Value, error) { 87 | return int64(i32), nil 88 | } 89 | 90 | // NewInt32 returns a new Int32 91 | func NewInt32(input int32) Int32 { 92 | i32 := Int32(input) 93 | return i32 94 | } 95 | -------------------------------------------------------------------------------- /types/int64.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | // Int64 is a type alias for the Go basic type int64 for use as an RVType 10 | type Int64 int64 11 | 12 | // WriteTo writes the Int64 to the given writable 13 | func (i64 Int64) WriteTo(writable Writable) { 14 | writable.WriteInt64LE(int64(i64)) 15 | } 16 | 17 | // ExtractFrom extracts the Int64 value from the given readable 18 | func (i64 *Int64) ExtractFrom(readable Readable) error { 19 | value, err := readable.ReadInt64LE() 20 | if err != nil { 21 | return err 22 | } 23 | 24 | *i64 = Int64(value) 25 | return nil 26 | } 27 | 28 | // Copy returns a pointer to a copy of the Int64. Requires type assertion when used 29 | func (i64 Int64) Copy() RVType { 30 | return NewInt64(int64(i64)) 31 | } 32 | 33 | // Equals checks if the input is equal in value to the current instance 34 | func (i64 Int64) Equals(o RVType) bool { 35 | other, ok := o.(Int64) 36 | if !ok { 37 | return false 38 | } 39 | 40 | return i64 == other 41 | } 42 | 43 | // CopyRef copies the current value of the Int64 44 | // and returns a pointer to the new copy 45 | func (i64 Int64) CopyRef() RVTypePtr { 46 | copied := i64.Copy().(Int64) 47 | return &copied 48 | } 49 | 50 | // Deref takes a pointer to the Int64 51 | // and dereferences it to the raw value. 52 | // Only useful when working with an instance of RVTypePtr 53 | func (i64 *Int64) Deref() RVType { 54 | return *i64 55 | } 56 | 57 | // String returns a string representation of the Int64 58 | func (i64 Int64) String() string { 59 | return fmt.Sprintf("%d", i64) 60 | } 61 | 62 | // Scan implements sql.Scanner for database/sql 63 | func (i64 *Int64) Scan(value any) error { 64 | if value == nil { 65 | *i64 = Int64(0) 66 | return nil 67 | } 68 | 69 | switch v := value.(type) { 70 | case int64: 71 | *i64 = Int64(v) 72 | case string: 73 | parsed, err := strconv.ParseInt(v, 10, 64) 74 | if err != nil { 75 | return fmt.Errorf("cannot parse string %q into Int64: %w", v, err) 76 | } 77 | *i64 = Int64(parsed) 78 | default: 79 | return fmt.Errorf("cannot scan %T into Int64", value) 80 | } 81 | 82 | return nil 83 | } 84 | 85 | // Value implements driver.Valuer for database/sql 86 | func (i64 Int64) Value() (driver.Value, error) { 87 | return int64(i64), nil 88 | } 89 | 90 | // NewInt64 returns a new Int64 91 | func NewInt64(input int64) Int64 { 92 | i64 := Int64(input) 93 | return i64 94 | } 95 | -------------------------------------------------------------------------------- /types/double.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | // Double is a type alias for the Go basic type float64 for use as an RVType 10 | type Double float64 11 | 12 | // WriteTo writes the Double to the given writable 13 | func (d Double) WriteTo(writable Writable) { 14 | writable.WriteFloat64LE(float64(d)) 15 | } 16 | 17 | // ExtractFrom extracts the Double value from the given readable 18 | func (d *Double) ExtractFrom(readable Readable) error { 19 | value, err := readable.ReadFloat64LE() 20 | if err != nil { 21 | return err 22 | } 23 | 24 | *d = Double(value) 25 | return nil 26 | } 27 | 28 | // Copy returns a pointer to a copy of the Double. Requires type assertion when used 29 | func (d Double) Copy() RVType { 30 | return NewDouble(float64(d)) 31 | } 32 | 33 | // Equals checks if the input is equal in value to the current instance 34 | func (d Double) Equals(o RVType) bool { 35 | other, ok := o.(Double) 36 | if !ok { 37 | return false 38 | } 39 | 40 | return d == other 41 | } 42 | 43 | // CopyRef copies the current value of the Double 44 | // and returns a pointer to the new copy 45 | func (d Double) CopyRef() RVTypePtr { 46 | copied := d.Copy().(Double) 47 | return &copied 48 | } 49 | 50 | // Deref takes a pointer to the Double 51 | // and dereferences it to the raw value. 52 | // Only useful when working with an instance of RVTypePtr 53 | func (d *Double) Deref() RVType { 54 | return *d 55 | } 56 | 57 | // String returns a string representation of the Double 58 | func (d Double) String() string { 59 | return fmt.Sprintf("%f", d) 60 | } 61 | 62 | // Scan implements sql.Scanner for database/sql 63 | func (d *Double) Scan(value any) error { 64 | if value == nil { 65 | *d = Double(0.0) 66 | return nil 67 | } 68 | 69 | switch v := value.(type) { 70 | case float64: 71 | *d = Double(v) 72 | case string: 73 | parsed, err := strconv.ParseFloat(v, 64) 74 | if err != nil { 75 | return fmt.Errorf("cannot parse string %q into Double: %w", v, err) 76 | } 77 | *d = Double(parsed) 78 | default: 79 | return fmt.Errorf("cannot scan %T into Double", value) 80 | } 81 | 82 | return nil 83 | } 84 | 85 | // Value implements driver.Valuer for database/sql 86 | func (d Double) Value() (driver.Value, error) { 87 | return float64(d), nil 88 | } 89 | 90 | // NewDouble returns a new Double 91 | func NewDouble(input float64) Double { 92 | d := Double(input) 93 | return d 94 | } 95 | -------------------------------------------------------------------------------- /types/uint16.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | // UInt16 is a type alias for the Go basic type uint16 for use as an RVType 10 | type UInt16 uint16 11 | 12 | // WriteTo writes the UInt16 to the given writable 13 | func (u16 UInt16) WriteTo(writable Writable) { 14 | writable.WriteUInt16LE(uint16(u16)) 15 | } 16 | 17 | // ExtractFrom extracts the UInt16 value from the given readable 18 | func (u16 *UInt16) ExtractFrom(readable Readable) error { 19 | value, err := readable.ReadUInt16LE() 20 | if err != nil { 21 | return err 22 | } 23 | 24 | *u16 = UInt16(value) 25 | return nil 26 | } 27 | 28 | // Copy returns a pointer to a copy of the UInt16. Requires type assertion when used 29 | func (u16 UInt16) Copy() RVType { 30 | return NewUInt16(uint16(u16)) 31 | } 32 | 33 | // Equals checks if the input is equal in value to the current instance 34 | func (u16 UInt16) Equals(o RVType) bool { 35 | other, ok := o.(UInt16) 36 | if !ok { 37 | return false 38 | } 39 | 40 | return u16 == other 41 | } 42 | 43 | // CopyRef copies the current value of the UInt16 44 | // and returns a pointer to the new copy 45 | func (u16 UInt16) CopyRef() RVTypePtr { 46 | copied := u16.Copy().(UInt16) 47 | return &copied 48 | } 49 | 50 | // Deref takes a pointer to the UInt16 51 | // and dereferences it to the raw value. 52 | // Only useful when working with an instance of RVTypePtr 53 | func (u16 *UInt16) Deref() RVType { 54 | return *u16 55 | } 56 | 57 | // String returns a string representation of the UInt16 58 | func (u16 UInt16) String() string { 59 | return fmt.Sprintf("%d", u16) 60 | } 61 | 62 | // Scan implements sql.Scanner for database/sql 63 | func (u16 *UInt16) Scan(value any) error { 64 | if value == nil { 65 | *u16 = UInt16(0) 66 | return nil 67 | } 68 | 69 | switch v := value.(type) { 70 | case int64: 71 | *u16 = UInt16(v) 72 | case string: 73 | parsed, err := strconv.ParseUint(v, 10, 16) 74 | if err != nil { 75 | return fmt.Errorf("cannot parse string %q into UInt16: %w", v, err) 76 | } 77 | *u16 = UInt16(parsed) 78 | default: 79 | return fmt.Errorf("cannot scan %T into UInt16", value) 80 | } 81 | 82 | return nil 83 | } 84 | 85 | // Value implements driver.Valuer for database/sql 86 | func (u16 UInt16) Value() (driver.Value, error) { 87 | return int64(u16), nil 88 | } 89 | 90 | // NewUInt16 returns a new UInt16 91 | func NewUInt16(input uint16) UInt16 { 92 | u16 := UInt16(input) 93 | return u16 94 | } 95 | -------------------------------------------------------------------------------- /types/uint32.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | // UInt32 is a type alias for the Go basic type uint32 for use as an RVType 10 | type UInt32 uint32 11 | 12 | // WriteTo writes the UInt32 to the given writable 13 | func (u32 UInt32) WriteTo(writable Writable) { 14 | writable.WriteUInt32LE(uint32(u32)) 15 | } 16 | 17 | // ExtractFrom extracts the UInt32 value from the given readable 18 | func (u32 *UInt32) ExtractFrom(readable Readable) error { 19 | value, err := readable.ReadUInt32LE() 20 | if err != nil { 21 | return err 22 | } 23 | 24 | *u32 = UInt32(value) 25 | return nil 26 | } 27 | 28 | // Copy returns a pointer to a copy of the UInt32. Requires type assertion when used 29 | func (u32 UInt32) Copy() RVType { 30 | return NewUInt32(uint32(u32)) 31 | } 32 | 33 | // Equals checks if the input is equal in value to the current instance 34 | func (u32 UInt32) Equals(o RVType) bool { 35 | other, ok := o.(UInt32) 36 | if !ok { 37 | return false 38 | } 39 | 40 | return u32 == other 41 | } 42 | 43 | // CopyRef copies the current value of the UInt32 44 | // and returns a pointer to the new copy 45 | func (u32 UInt32) CopyRef() RVTypePtr { 46 | copied := u32.Copy().(UInt32) 47 | return &copied 48 | } 49 | 50 | // Deref takes a pointer to the UInt32 51 | // and dereferences it to the raw value. 52 | // Only useful when working with an instance of RVTypePtr 53 | func (u32 *UInt32) Deref() RVType { 54 | return *u32 55 | } 56 | 57 | // String returns a string representation of the UInt32 58 | func (u32 UInt32) String() string { 59 | return fmt.Sprintf("%d", u32) 60 | } 61 | 62 | // Scan implements sql.Scanner for database/sql 63 | func (u32 *UInt32) Scan(value any) error { 64 | if value == nil { 65 | *u32 = UInt32(0) 66 | return nil 67 | } 68 | 69 | switch v := value.(type) { 70 | case int64: 71 | *u32 = UInt32(v) 72 | case string: 73 | parsed, err := strconv.ParseUint(v, 10, 32) 74 | if err != nil { 75 | return fmt.Errorf("cannot parse string %q into UInt32: %w", v, err) 76 | } 77 | *u32 = UInt32(parsed) 78 | default: 79 | return fmt.Errorf("cannot scan %T into UInt32", value) 80 | } 81 | 82 | return nil 83 | } 84 | 85 | // Value implements driver.Valuer for database/sql 86 | func (u32 UInt32) Value() (driver.Value, error) { 87 | return int64(u32), nil 88 | } 89 | 90 | // NewUInt32 returns a new UInt32 91 | func NewUInt32(input uint32) UInt32 { 92 | u32 := UInt32(input) 93 | return u32 94 | } 95 | -------------------------------------------------------------------------------- /encryption/rc4.go: -------------------------------------------------------------------------------- 1 | package encryption 2 | 3 | import ( 4 | "crypto/rc4" 5 | ) 6 | 7 | // RC4 encrypts data with RC4 8 | type RC4 struct { 9 | key []byte 10 | cipher *rc4.Cipher 11 | decipher *rc4.Cipher 12 | cipheredCount uint64 13 | decipheredCount uint64 14 | } 15 | 16 | // Key returns the crypto key 17 | func (r *RC4) Key() []byte { 18 | return r.key 19 | } 20 | 21 | // SetKey sets the crypto key and updates the ciphers 22 | func (r *RC4) SetKey(key []byte) error { 23 | r.key = key 24 | 25 | cipher, err := rc4.NewCipher(key) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | decipher, err := rc4.NewCipher(key) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | r.cipher = cipher 36 | r.decipher = decipher 37 | 38 | return nil 39 | } 40 | 41 | // Encrypt encrypts the payload with the outgoing RC4 stream 42 | func (r *RC4) Encrypt(payload []byte) ([]byte, error) { 43 | ciphered := make([]byte, len(payload)) 44 | 45 | r.cipher.XORKeyStream(ciphered, payload) 46 | 47 | r.cipheredCount += uint64(len(payload)) 48 | 49 | return ciphered, nil 50 | } 51 | 52 | // Decrypt decrypts the payload with the incoming RC4 stream 53 | func (r *RC4) Decrypt(payload []byte) ([]byte, error) { 54 | deciphered := make([]byte, len(payload)) 55 | 56 | r.decipher.XORKeyStream(deciphered, payload) 57 | 58 | r.decipheredCount += uint64(len(payload)) 59 | 60 | return deciphered, nil 61 | } 62 | 63 | // Copy returns a copy of the algorithm while retaining it's state 64 | func (r *RC4) Copy() Algorithm { 65 | copied := NewRC4Encryption() 66 | 67 | copied.SetKey(r.key) 68 | 69 | // * crypto/rc4 does not expose a way to directly copy streams and retain their state. 70 | // * This just discards the number of iterations done in the original ciphers to sync 71 | // * the copied ciphers states to the original 72 | for i := 0; i < int(r.cipheredCount); i++ { 73 | copied.cipher.XORKeyStream([]byte{0}, []byte{0}) 74 | } 75 | 76 | for i := 0; i < int(r.decipheredCount); i++ { 77 | copied.decipher.XORKeyStream([]byte{0}, []byte{0}) 78 | } 79 | 80 | copied.cipheredCount = r.cipheredCount 81 | copied.decipheredCount = r.decipheredCount 82 | 83 | return copied 84 | } 85 | 86 | // NewRC4Encryption returns a new instance of the RC4 encryption 87 | func NewRC4Encryption() *RC4 { 88 | encryption := &RC4{ 89 | key: make([]byte, 0), 90 | } 91 | 92 | encryption.SetKey([]byte("CD&ML")) // TODO - Make this configurable? 93 | 94 | return encryption 95 | } 96 | -------------------------------------------------------------------------------- /types/readable.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // Readable represents a struct that types can read from 4 | type Readable interface { 5 | StringLengthSize() int // Returns the size of the length field for rdv::String types. Only 2 and 4 are valid 6 | PIDSize() int // Returns the size of the length fields for nn::nex::PID types. Only 4 and 8 are valid 7 | UseStructureHeader() bool // Returns whether or not Structure types should use a header 8 | Remaining() uint64 // Returns the number of bytes left unread in the buffer 9 | ReadRemaining() []byte // Reads the remaining data from the buffer 10 | Read(length uint64) ([]byte, error) // Reads up to length bytes of data from the buffer. Returns an error if the read failed, such as if there was not enough data to read 11 | ReadUInt8() (uint8, error) // Reads a primitive Go uint8. Returns an error if the read failed, such as if there was not enough data to read 12 | ReadUInt16LE() (uint16, error) // Reads a primitive Go uint16. Returns an error if the read failed, such as if there was not enough data to read 13 | ReadUInt32LE() (uint32, error) // Reads a primitive Go uint32. Returns an error if the read failed, such as if there was not enough data to read 14 | ReadUInt64LE() (uint64, error) // Reads a primitive Go uint64. Returns an error if the read failed, such as if there was not enough data to read 15 | ReadInt8() (int8, error) // Reads a primitive Go int8. Returns an error if the read failed, such as if there was not enough data to read 16 | ReadInt16LE() (int16, error) // Reads a primitive Go int16. Returns an error if the read failed, such as if there was not enough data to read 17 | ReadInt32LE() (int32, error) // Reads a primitive Go int32. Returns an error if the read failed, such as if there was not enough data to read 18 | ReadInt64LE() (int64, error) // Reads a primitive Go int64. Returns an error if the read failed, such as if there was not enough data to read 19 | ReadFloat32LE() (float32, error) // Reads a primitive Go float32. Returns an error if the read failed, such as if there was not enough data to read 20 | ReadFloat64LE() (float64, error) // Reads a primitive Go float64. Returns an error if the read failed, such as if there was not enough data to read 21 | ReadBool() (bool, error) // Reads a primitive Go bool. Returns an error if the read failed, such as if there was not enough data to read 22 | } 23 | -------------------------------------------------------------------------------- /encryption/quazal_rc4.go: -------------------------------------------------------------------------------- 1 | package encryption 2 | 3 | import ( 4 | "crypto/rc4" 5 | ) 6 | 7 | // QuazalRC4 encrypts data with RC4. Each iteration uses a new cipher instance. The key is always CD&ML 8 | type QuazalRC4 struct { 9 | key []byte 10 | cipher *rc4.Cipher 11 | decipher *rc4.Cipher 12 | cipheredCount uint64 13 | decipheredCount uint64 14 | } 15 | 16 | // Key returns the crypto key 17 | func (r *QuazalRC4) Key() []byte { 18 | return r.key 19 | } 20 | 21 | // SetKey sets the crypto key and updates the ciphers 22 | func (r *QuazalRC4) SetKey(key []byte) error { 23 | r.key = key 24 | 25 | cipher, err := rc4.NewCipher(key) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | decipher, err := rc4.NewCipher(key) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | r.cipher = cipher 36 | r.decipher = decipher 37 | 38 | return nil 39 | } 40 | 41 | // Encrypt encrypts the payload with the outgoing QuazalRC4 stream 42 | func (r *QuazalRC4) Encrypt(payload []byte) ([]byte, error) { 43 | r.SetKey([]byte("CD&ML")) 44 | 45 | ciphered := make([]byte, len(payload)) 46 | 47 | r.cipher.XORKeyStream(ciphered, payload) 48 | 49 | r.cipheredCount += uint64(len(payload)) 50 | 51 | return ciphered, nil 52 | } 53 | 54 | // Decrypt decrypts the payload with the incoming QuazalRC4 stream 55 | func (r *QuazalRC4) Decrypt(payload []byte) ([]byte, error) { 56 | r.SetKey([]byte("CD&ML")) 57 | 58 | deciphered := make([]byte, len(payload)) 59 | 60 | r.decipher.XORKeyStream(deciphered, payload) 61 | 62 | r.decipheredCount += uint64(len(payload)) 63 | 64 | return deciphered, nil 65 | } 66 | 67 | // Copy returns a copy of the algorithm while retaining it's state 68 | func (r *QuazalRC4) Copy() Algorithm { 69 | copied := NewQuazalRC4Encryption() 70 | 71 | copied.SetKey(r.key) 72 | 73 | // * crypto/rc4 does not expose a way to directly copy streams and retain their state. 74 | // * This just discards the number of iterations done in the original ciphers to sync 75 | // * the copied ciphers states to the original 76 | for i := 0; i < int(r.cipheredCount); i++ { 77 | copied.cipher.XORKeyStream([]byte{0}, []byte{0}) 78 | } 79 | 80 | for i := 0; i < int(r.decipheredCount); i++ { 81 | copied.decipher.XORKeyStream([]byte{0}, []byte{0}) 82 | } 83 | 84 | copied.cipheredCount = r.cipheredCount 85 | copied.decipheredCount = r.decipheredCount 86 | 87 | return copied 88 | } 89 | 90 | // NewQuazalRC4Encryption returns a new instance of the QuazalRC4 encryption 91 | func NewQuazalRC4Encryption() *QuazalRC4 { 92 | return &QuazalRC4{ 93 | key: make([]byte, 0), 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /types/data.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Data is an implementation of rdv::Data. 9 | // This structure has no data, and instead acts as the base class for many other structures. 10 | type Data struct { 11 | Structure 12 | } 13 | 14 | // ObjectID returns the object identifier of the type 15 | func (d Data) ObjectID() RVType { 16 | return d.DataObjectID() 17 | } 18 | 19 | // DataObjectID returns the object identifier of the type embedding Data 20 | func (d Data) DataObjectID() RVType { 21 | return NewString("Data") 22 | } 23 | 24 | // WriteTo writes the Data to the given writable 25 | func (d Data) WriteTo(writable Writable) { 26 | d.WriteHeaderTo(writable, 0) 27 | } 28 | 29 | // ExtractFrom extracts the Data from the given readable 30 | func (d *Data) ExtractFrom(readable Readable) error { 31 | if err := d.ExtractHeaderFrom(readable); err != nil { 32 | return fmt.Errorf("Failed to read Data header. %s", err.Error()) 33 | } 34 | 35 | return nil 36 | } 37 | 38 | // Copy returns a pointer to a copy of the Data. Requires type assertion when used 39 | func (d Data) Copy() RVType { 40 | copied := NewData() 41 | copied.StructureVersion = d.StructureVersion 42 | 43 | return copied 44 | } 45 | 46 | // Equals checks if the input is equal in value to the current instance 47 | func (d Data) Equals(o RVType) bool { 48 | if _, ok := o.(Data); !ok { 49 | return false 50 | } 51 | 52 | other := o.(Data) 53 | 54 | return d.StructureVersion == other.StructureVersion 55 | } 56 | 57 | // CopyRef copies the current value of the Data 58 | // and returns a pointer to the new copy 59 | func (d Data) CopyRef() RVTypePtr { 60 | copied := d.Copy().(Data) 61 | return &copied 62 | } 63 | 64 | // Deref takes a pointer to the Data 65 | // and dereferences it to the raw value. 66 | // Only useful when working with an instance of RVTypePtr 67 | func (d *Data) Deref() RVType { 68 | return *d 69 | } 70 | 71 | // String returns a string representation of the struct 72 | func (d Data) String() string { 73 | return d.FormatToString(0) 74 | } 75 | 76 | // FormatToString pretty-prints the struct data using the provided indentation level 77 | func (d Data) FormatToString(indentationLevel int) string { 78 | indentationValues := strings.Repeat("\t", indentationLevel+1) 79 | indentationEnd := strings.Repeat("\t", indentationLevel) 80 | 81 | var b strings.Builder 82 | 83 | b.WriteString("Data{\n") 84 | b.WriteString(fmt.Sprintf("%sStructureVersion: %d\n", indentationValues, d.StructureVersion)) 85 | b.WriteString(fmt.Sprintf("%s}", indentationEnd)) 86 | 87 | return b.String() 88 | } 89 | 90 | // NewData returns a new Data Structure 91 | func NewData() Data { 92 | return Data{} 93 | } 94 | -------------------------------------------------------------------------------- /types/buffer.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "bytes" 5 | "database/sql/driver" 6 | "encoding/hex" 7 | "fmt" 8 | ) 9 | 10 | // Buffer is an implementation of rdv::Buffer. 11 | // Type alias of []byte. 12 | // Same as QBuffer but with a uint32 length field. 13 | type Buffer []byte 14 | 15 | // WriteTo writes the Buffer to the given writable 16 | func (b Buffer) WriteTo(writable Writable) { 17 | length := len(b) 18 | 19 | writable.WriteUInt32LE(uint32(length)) 20 | 21 | if length > 0 { 22 | writable.Write(b) 23 | } 24 | } 25 | 26 | // ExtractFrom extracts the Buffer from the given readable 27 | func (b *Buffer) ExtractFrom(readable Readable) error { 28 | length, err := readable.ReadUInt32LE() 29 | if err != nil { 30 | return fmt.Errorf("Failed to read NEX Buffer length. %s", err.Error()) 31 | } 32 | 33 | value, err := readable.Read(uint64(length)) 34 | if err != nil { 35 | return fmt.Errorf("Failed to read NEX Buffer data. %s", err.Error()) 36 | } 37 | 38 | *b = Buffer(value) 39 | return nil 40 | } 41 | 42 | // Copy returns a pointer to a copy of the Buffer. Requires type assertion when used 43 | func (b Buffer) Copy() RVType { 44 | return NewBuffer(b) 45 | } 46 | 47 | // Equals checks if the input is equal in value to the current instance 48 | func (b Buffer) Equals(o RVType) bool { 49 | if _, ok := o.(Buffer); !ok { 50 | return false 51 | } 52 | 53 | return bytes.Equal(b, o.(Buffer)) 54 | } 55 | 56 | // CopyRef copies the current value of the Buffer 57 | // and returns a pointer to the new copy 58 | func (b Buffer) CopyRef() RVTypePtr { 59 | copied := b.Copy().(Buffer) 60 | return &copied 61 | } 62 | 63 | // Deref takes a pointer to the Buffer 64 | // and dereferences it to the raw value. 65 | // Only useful when working with an instance of RVTypePtr 66 | func (b *Buffer) Deref() RVType { 67 | return *b 68 | } 69 | 70 | // String returns a string representation of the struct 71 | func (b Buffer) String() string { 72 | return hex.EncodeToString(b) 73 | } 74 | 75 | // Scan implements sql.Scanner for database/sql 76 | func (b *Buffer) Scan(value any) error { 77 | if value == nil { 78 | *b = Buffer([]byte{}) 79 | return nil 80 | } 81 | 82 | if _, ok := value.([]byte); !ok { 83 | return fmt.Errorf("cannot scan %T into Buffer", value) 84 | } 85 | 86 | *b = Buffer(value.([]byte)) 87 | 88 | return nil 89 | } 90 | 91 | // Value implements driver.Valuer for database/sql 92 | func (b Buffer) Value() (driver.Value, error) { 93 | return []byte(b), nil 94 | } 95 | 96 | // NewBuffer returns a new Buffer 97 | func NewBuffer(input []byte) Buffer { 98 | b := make(Buffer, len(input)) 99 | copy(b, input) 100 | 101 | return b 102 | } 103 | -------------------------------------------------------------------------------- /types/qbuffer.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "bytes" 5 | "database/sql/driver" 6 | "encoding/hex" 7 | "fmt" 8 | ) 9 | 10 | // QBuffer is an implementation of rdv::qBuffer. 11 | // Type alias of []byte. 12 | // Same as Buffer but with a uint16 length field. 13 | type QBuffer []byte 14 | 15 | // WriteTo writes the []byte to the given writable 16 | func (qb QBuffer) WriteTo(writable Writable) { 17 | length := len(qb) 18 | 19 | writable.WriteUInt16LE(uint16(length)) 20 | 21 | if length > 0 { 22 | writable.Write(qb) 23 | } 24 | } 25 | 26 | // ExtractFrom extracts the QBuffer from the given readable 27 | func (qb *QBuffer) ExtractFrom(readable Readable) error { 28 | length, err := readable.ReadUInt16LE() 29 | if err != nil { 30 | return fmt.Errorf("Failed to read NEX qBuffer length. %s", err.Error()) 31 | } 32 | 33 | data, err := readable.Read(uint64(length)) 34 | if err != nil { 35 | return fmt.Errorf("Failed to read NEX qBuffer data. %s", err.Error()) 36 | } 37 | 38 | *qb = data 39 | return nil 40 | } 41 | 42 | // Copy returns a pointer to a copy of the qBuffer. Requires type assertion when used 43 | func (qb QBuffer) Copy() RVType { 44 | return NewQBuffer(qb) 45 | } 46 | 47 | // Equals checks if the input is equal in value to the current instance 48 | func (qb QBuffer) Equals(o RVType) bool { 49 | if _, ok := o.(QBuffer); !ok { 50 | return false 51 | } 52 | 53 | return bytes.Equal(qb, o.(QBuffer)) 54 | } 55 | 56 | // CopyRef copies the current value of the QBuffer 57 | // and returns a pointer to the new copy 58 | func (qb QBuffer) CopyRef() RVTypePtr { 59 | copied := qb.Copy().(QBuffer) 60 | return &copied 61 | } 62 | 63 | // Deref takes a pointer to the QBuffer 64 | // and dereferences it to the raw value. 65 | // Only useful when working with an instance of RVTypePtr 66 | func (qb *QBuffer) Deref() RVType { 67 | return *qb 68 | } 69 | 70 | // String returns a string representation of the struct 71 | func (qb QBuffer) String() string { 72 | return hex.EncodeToString(qb) 73 | } 74 | 75 | // Scan implements sql.Scanner for database/sql 76 | func (qb *QBuffer) Scan(value any) error { 77 | if value == nil { 78 | *qb = QBuffer([]byte{}) 79 | return nil 80 | } 81 | 82 | if _, ok := value.([]byte); !ok { 83 | return fmt.Errorf("cannot scan %T into QBuffer", value) 84 | } 85 | 86 | *qb = QBuffer(value.([]byte)) 87 | 88 | return nil 89 | } 90 | 91 | // Value implements driver.Valuer for database/sql 92 | func (qb QBuffer) Value() (driver.Value, error) { 93 | return []byte(qb), nil 94 | } 95 | 96 | // NewQBuffer returns a new QBuffer 97 | func NewQBuffer(input []byte) QBuffer { 98 | qb := make(QBuffer, len(input)) 99 | copy(qb, input) 100 | 101 | return qb 102 | } 103 | -------------------------------------------------------------------------------- /types/uint64.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | // UInt64 is a type alias for the Go basic type uint64 for use as an RVType 10 | type UInt64 uint64 11 | 12 | // WriteTo writes the UInt64 to the given writable 13 | func (u64 UInt64) WriteTo(writable Writable) { 14 | writable.WriteUInt64LE(uint64(u64)) 15 | } 16 | 17 | // ExtractFrom extracts the UInt64 value from the given readable 18 | func (u64 *UInt64) ExtractFrom(readable Readable) error { 19 | value, err := readable.ReadUInt64LE() 20 | if err != nil { 21 | return err 22 | } 23 | 24 | *u64 = UInt64(value) 25 | return nil 26 | } 27 | 28 | // Copy returns a pointer to a copy of the UInt64. Requires type assertion when used 29 | func (u64 UInt64) Copy() RVType { 30 | return NewUInt64(uint64(u64)) 31 | } 32 | 33 | // Equals checks if the input is equal in value to the current instance 34 | func (u64 UInt64) Equals(o RVType) bool { 35 | other, ok := o.(UInt64) 36 | if !ok { 37 | return false 38 | } 39 | 40 | return u64 == other 41 | } 42 | 43 | // CopyRef copies the current value of the UInt64 44 | // and returns a pointer to the new copy 45 | func (u64 UInt64) CopyRef() RVTypePtr { 46 | copied := u64.Copy().(UInt64) 47 | return &copied 48 | } 49 | 50 | // Deref takes a pointer to the UInt64 51 | // and dereferences it to the raw value. 52 | // Only useful when working with an instance of RVTypePtr 53 | func (u64 *UInt64) Deref() RVType { 54 | return *u64 55 | } 56 | 57 | // String returns a string representation of the UInt64 58 | func (u64 UInt64) String() string { 59 | return fmt.Sprintf("%d", u64) 60 | } 61 | 62 | // Scan implements sql.Scanner for database/sql 63 | func (u64 *UInt64) Scan(value any) error { 64 | if value == nil { 65 | *u64 = UInt64(0) 66 | return nil 67 | } 68 | 69 | switch v := value.(type) { 70 | case int64: // * Postgres might store/return the data as an int64 71 | *u64 = UInt64(uint64(v)) 72 | case []byte, string: // * Otherwise, it's stored/returned as a string 73 | var str string 74 | if b, ok := v.([]byte); ok { 75 | str = string(b) 76 | } else { 77 | str = v.(string) 78 | } 79 | 80 | parsed, err := strconv.ParseUint(str, 10, 64) 81 | if err != nil { 82 | return fmt.Errorf("cannot parse string %q into UInt64: %w", v, err) 83 | } 84 | *u64 = UInt64(parsed) 85 | default: 86 | return fmt.Errorf("cannot scan %T into UInt64", value) 87 | } 88 | 89 | return nil 90 | } 91 | 92 | // Value implements driver.Valuer for database/sql 93 | func (u64 UInt64) Value() (driver.Value, error) { 94 | return fmt.Sprintf("%d", uint64(u64)), nil 95 | } 96 | 97 | // NewUInt64 returns a new UInt64 98 | func NewUInt64(input uint64) UInt64 { 99 | u64 := UInt64(input) 100 | return u64 101 | } 102 | -------------------------------------------------------------------------------- /types/class_version_container.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // ClassVersionContainer is an implementation of rdv::ClassVersionContainer. 9 | // Contains version info for Structures used in verbose RMC messages. 10 | type ClassVersionContainer struct { 11 | Structure 12 | ClassVersions Map[String, UInt16] `json:"class_versions" db:"class_versions" bson:"class_versions" xml:"ClassVersions"` 13 | } 14 | 15 | // WriteTo writes the ClassVersionContainer to the given writable 16 | func (cvc ClassVersionContainer) WriteTo(writable Writable) { 17 | cvc.ClassVersions.WriteTo(writable) 18 | } 19 | 20 | // ExtractFrom extracts the ClassVersionContainer from the given readable 21 | func (cvc *ClassVersionContainer) ExtractFrom(readable Readable) error { 22 | return cvc.ClassVersions.ExtractFrom(readable) 23 | } 24 | 25 | // Copy returns a pointer to a copy of the ClassVersionContainer. Requires type assertion when used 26 | func (cvc ClassVersionContainer) Copy() RVType { 27 | copied := NewClassVersionContainer() 28 | copied.ClassVersions = cvc.ClassVersions.Copy().(Map[String, UInt16]) 29 | 30 | return copied 31 | } 32 | 33 | // Equals checks if the input is equal in value to the current instance 34 | func (cvc ClassVersionContainer) Equals(o RVType) bool { 35 | if _, ok := o.(ClassVersionContainer); !ok { 36 | return false 37 | } 38 | 39 | return cvc.ClassVersions.Equals(o) 40 | } 41 | 42 | // CopyRef copies the current value of the ClassVersionContainer 43 | // and returns a pointer to the new copy 44 | func (cvc ClassVersionContainer) CopyRef() RVTypePtr { 45 | copied := cvc.Copy().(ClassVersionContainer) 46 | return &copied 47 | } 48 | 49 | // Deref takes a pointer to the ClassVersionContainer 50 | // and dereferences it to the raw value. 51 | // Only useful when working with an instance of RVTypePtr 52 | func (cvc *ClassVersionContainer) Deref() RVType { 53 | return *cvc 54 | } 55 | 56 | // String returns a string representation of the struct 57 | func (cvc ClassVersionContainer) String() string { 58 | return cvc.FormatToString(0) 59 | } 60 | 61 | // FormatToString pretty-prints the struct data using the provided indentation level 62 | func (cvc ClassVersionContainer) FormatToString(indentationLevel int) string { 63 | indentationValues := strings.Repeat("\t", indentationLevel+1) 64 | indentationEnd := strings.Repeat("\t", indentationLevel) 65 | 66 | var b strings.Builder 67 | 68 | b.WriteString("ClassVersionContainer{\n") 69 | b.WriteString(fmt.Sprintf("%sStructureVersion: %d,\n", indentationValues, cvc.StructureVersion)) 70 | b.WriteString(fmt.Sprintf("%sClassVersions: %s\n", indentationValues, cvc.ClassVersions)) 71 | b.WriteString(fmt.Sprintf("%s}", indentationEnd)) 72 | 73 | return b.String() 74 | } 75 | 76 | // NewClassVersionContainer returns a new ClassVersionContainer 77 | func NewClassVersionContainer() ClassVersionContainer { 78 | cvc := ClassVersionContainer{ 79 | ClassVersions: NewMap[String, UInt16](), 80 | } 81 | 82 | return cvc 83 | } 84 | -------------------------------------------------------------------------------- /types/string.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | // String is an implementation of rdv::String. 11 | // Type alias of string 12 | type String string 13 | 14 | // WriteTo writes the String to the given writable 15 | func (s String) WriteTo(writable Writable) { 16 | s = s + "\x00" 17 | strLength := len(s) 18 | 19 | if writable.StringLengthSize() == 4 { 20 | writable.WriteUInt32LE(uint32(strLength)) 21 | } else { 22 | writable.WriteUInt16LE(uint16(strLength)) 23 | } 24 | 25 | writable.Write([]byte(s)) 26 | } 27 | 28 | // ExtractFrom extracts the String from the given readable 29 | func (s *String) ExtractFrom(readable Readable) error { 30 | var length uint64 31 | var err error 32 | 33 | if readable.StringLengthSize() == 4 { 34 | l, e := readable.ReadUInt32LE() 35 | length = uint64(l) 36 | err = e 37 | } else { 38 | l, e := readable.ReadUInt16LE() 39 | length = uint64(l) 40 | err = e 41 | } 42 | 43 | if err != nil { 44 | return fmt.Errorf("Failed to read NEX string length. %s", err.Error()) 45 | } 46 | 47 | if readable.Remaining() < length { 48 | return errors.New("NEX string length longer than data size") 49 | } 50 | 51 | stringData, err := readable.Read(length) 52 | if err != nil { 53 | return fmt.Errorf("Failed to read NEX string length. %s", err.Error()) 54 | } 55 | 56 | str := strings.TrimRight(string(stringData), "\x00") 57 | 58 | *s = String(str) 59 | return nil 60 | } 61 | 62 | // Copy returns a pointer to a copy of the String. Requires type assertion when used 63 | func (s String) Copy() RVType { 64 | return NewString(string(s)) 65 | } 66 | 67 | // Equals checks if the input is equal in value to the current instance 68 | func (s String) Equals(o RVType) bool { 69 | if _, ok := o.(String); !ok { 70 | return false 71 | } 72 | 73 | return s == o.(String) 74 | } 75 | 76 | // CopyRef copies the current value of the String 77 | // and returns a pointer to the new copy 78 | func (s String) CopyRef() RVTypePtr { 79 | copied := s.Copy().(String) 80 | return &copied 81 | } 82 | 83 | // Deref takes a pointer to the String 84 | // and dereferences it to the raw value. 85 | // Only useful when working with an instance of RVTypePtr 86 | func (s *String) Deref() RVType { 87 | return *s 88 | } 89 | 90 | // String returns a string representation of the struct 91 | func (s String) String() string { 92 | return fmt.Sprintf("%q", string(s)) 93 | } 94 | 95 | // Scan implements sql.Scanner for database/sql 96 | func (s *String) Scan(value any) error { 97 | if value == nil { 98 | *s = String("") 99 | return nil 100 | } 101 | 102 | if _, ok := value.(string); !ok { 103 | return fmt.Errorf("cannot scan %T into String", value) 104 | } 105 | 106 | *s = String(value.(string)) 107 | 108 | return nil 109 | } 110 | 111 | // Value implements driver.Valuer for database/sql 112 | func (s String) Value() (driver.Value, error) { 113 | return string(s), nil 114 | } 115 | 116 | // NewString returns a new String 117 | func NewString(input string) String { 118 | s := String(input) 119 | return s 120 | } 121 | -------------------------------------------------------------------------------- /websocket_server.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/lxzan/gws" 8 | ) 9 | 10 | type wsEventHandler struct { 11 | prudpServer *PRUDPServer 12 | } 13 | 14 | func (wseh *wsEventHandler) OnOpen(socket *gws.Conn) {} 15 | 16 | func (wseh *wsEventHandler) OnClose(wsConn *gws.Conn, _ error) { 17 | // * Loop over all connections on all endpoints 18 | wseh.prudpServer.Endpoints.Each(func(streamid uint8, pep *PRUDPEndPoint) bool { 19 | connections := make([]*PRUDPConnection, 0) 20 | 21 | pep.Connections.Each(func(discriminator string, pc *PRUDPConnection) bool { 22 | if pc.Socket.Address == wsConn.RemoteAddr() { 23 | connections = append(connections, pc) 24 | } 25 | return false 26 | }) 27 | 28 | // * We cannot modify a MutexMap while looping over it 29 | // * since the mutex is locked. We first need to grab 30 | // * the entries we want to delete, and then loop over 31 | // * them here to actually clean them up 32 | for _, connection := range connections { 33 | pep.CleanupConnection(connection) // * "removed" event is dispatched here 34 | } 35 | return false 36 | }) 37 | } 38 | 39 | func (wseh *wsEventHandler) OnPing(socket *gws.Conn, payload []byte) { 40 | _ = socket.WritePong(nil) 41 | } 42 | 43 | func (wseh *wsEventHandler) OnPong(socket *gws.Conn, payload []byte) {} 44 | 45 | func (wseh *wsEventHandler) OnMessage(socket *gws.Conn, message *gws.Message) { 46 | defer message.Close() 47 | 48 | // * Create a COPY of the underlying *bytes.Buffer bytes. 49 | // * If this is not done, then the byte slice sometimes 50 | // * gets modified in unexpected places 51 | packetData := append([]byte(nil), message.Bytes()...) 52 | err := wseh.prudpServer.handleSocketMessage(packetData, socket.RemoteAddr(), socket) 53 | if err != nil { 54 | logger.Error(err.Error()) 55 | } 56 | } 57 | 58 | // WebSocketServer wraps a WebSocket server to create an easier API to consume 59 | type WebSocketServer struct { 60 | mux *http.ServeMux 61 | upgrader *gws.Upgrader 62 | prudpServer *PRUDPServer 63 | } 64 | 65 | func (ws *WebSocketServer) init() { 66 | ws.upgrader = gws.NewUpgrader(&wsEventHandler{ 67 | prudpServer: ws.prudpServer, 68 | }, &gws.ServerOption{ 69 | ParallelEnabled: true, // * Parallel message processing 70 | Recovery: gws.Recovery, // * Exception recovery 71 | ReadBufferSize: 64000, 72 | WriteBufferSize: 64000, 73 | }) 74 | 75 | ws.mux = http.NewServeMux() 76 | ws.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 77 | socket, err := ws.upgrader.Upgrade(w, r) 78 | if err != nil { 79 | return 80 | } 81 | 82 | go func() { 83 | socket.ReadLoop() // * Blocking prevents the context from being GC 84 | }() 85 | }) 86 | } 87 | 88 | func (ws *WebSocketServer) listen(port int) { 89 | ws.init() 90 | 91 | err := http.ListenAndServe(fmt.Sprintf(":%d", port), ws.mux) 92 | if err != nil { 93 | panic(err) 94 | } 95 | } 96 | 97 | func (ws *WebSocketServer) listenSecure(port int, certFile, keyFile string) { 98 | ws.init() 99 | 100 | err := http.ListenAndServeTLS(fmt.Sprintf(":%d", port), certFile, keyFile, ws.mux) 101 | if err != nil { 102 | panic(err) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /library_version.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "golang.org/x/mod/semver" 8 | ) 9 | 10 | // LibraryVersion represents a NEX library version 11 | type LibraryVersion struct { 12 | Major int 13 | Minor int 14 | Patch int 15 | GameSpecificPatch string 16 | semver string 17 | } 18 | 19 | // Copy returns a new copied instance of LibraryVersion 20 | func (lv *LibraryVersion) Copy() *LibraryVersion { 21 | return &LibraryVersion{ 22 | Major: lv.Major, 23 | Minor: lv.Minor, 24 | Patch: lv.Patch, 25 | GameSpecificPatch: lv.GameSpecificPatch, 26 | semver: fmt.Sprintf("v%d.%d.%d", lv.Major, lv.Minor, lv.Patch), 27 | } 28 | } 29 | 30 | func (lv *LibraryVersion) semverCompare(compare string) int { 31 | if !strings.HasPrefix(compare, "v") { 32 | // * Faster than doing "v" + string(compare) 33 | var b strings.Builder 34 | 35 | b.WriteString("v") 36 | b.WriteString(compare) 37 | 38 | compare = b.String() 39 | } 40 | 41 | if !semver.IsValid(compare) { 42 | // * The semver package returns 0 (equal) for invalid semvers in semver.Compare 43 | return 0 44 | } 45 | 46 | return semver.Compare(lv.semver, compare) 47 | } 48 | 49 | // GreaterOrEqual compares if the given semver is greater than or equal to the current version 50 | func (lv *LibraryVersion) GreaterOrEqual(compare string) bool { 51 | return lv.semverCompare(compare) != -1 52 | } 53 | 54 | // LessOrEqual compares if the given semver is lesser than or equal to the current version 55 | func (lv *LibraryVersion) LessOrEqual(compare string) bool { 56 | return lv.semverCompare(compare) != 1 57 | } 58 | 59 | // NewPatchedLibraryVersion returns a new LibraryVersion with a game specific patch 60 | func NewPatchedLibraryVersion(major, minor, patch int, gameSpecificPatch string) *LibraryVersion { 61 | return &LibraryVersion{ 62 | Major: major, 63 | Minor: minor, 64 | Patch: patch, 65 | GameSpecificPatch: gameSpecificPatch, 66 | semver: fmt.Sprintf("v%d.%d.%d", major, minor, patch), 67 | } 68 | } 69 | 70 | // NewLibraryVersion returns a new LibraryVersion 71 | func NewLibraryVersion(major, minor, patch int) *LibraryVersion { 72 | return &LibraryVersion{ 73 | Major: major, 74 | Minor: minor, 75 | Patch: patch, 76 | semver: fmt.Sprintf("v%d.%d.%d", major, minor, patch), 77 | } 78 | } 79 | 80 | // LibraryVersions contains a set of the NEX version that the server uses 81 | type LibraryVersions struct { 82 | Main *LibraryVersion 83 | DataStore *LibraryVersion 84 | MatchMaking *LibraryVersion 85 | Ranking *LibraryVersion 86 | Ranking2 *LibraryVersion 87 | Messaging *LibraryVersion 88 | Utility *LibraryVersion 89 | NATTraversal *LibraryVersion 90 | } 91 | 92 | // SetDefault sets the default NEX protocol versions 93 | func (lvs *LibraryVersions) SetDefault(version *LibraryVersion) { 94 | lvs.Main = version 95 | lvs.DataStore = version.Copy() 96 | lvs.MatchMaking = version.Copy() 97 | lvs.Ranking = version.Copy() 98 | lvs.Ranking2 = version.Copy() 99 | lvs.Messaging = version.Copy() 100 | lvs.Utility = version.Copy() 101 | lvs.NATTraversal = version.Copy() 102 | } 103 | 104 | // NewLibraryVersions returns a new set of LibraryVersions 105 | func NewLibraryVersions() *LibraryVersions { 106 | return &LibraryVersions{} 107 | } 108 | -------------------------------------------------------------------------------- /mutex_map.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import "sync" 4 | 5 | // MutexMap implements a map type with go routine safe accessors through mutex locks. Embeds sync.RWMutex 6 | type MutexMap[K comparable, V any] struct { 7 | *sync.RWMutex 8 | real map[K]V 9 | } 10 | 11 | // Set sets a key to a given value 12 | func (m *MutexMap[K, V]) Set(key K, value V) { 13 | m.Lock() 14 | defer m.Unlock() 15 | 16 | m.real[key] = value 17 | } 18 | 19 | // Get returns the given key value and a bool if found 20 | func (m *MutexMap[K, V]) Get(key K) (V, bool) { 21 | m.RLock() 22 | defer m.RUnlock() 23 | 24 | value, ok := m.real[key] 25 | 26 | return value, ok 27 | } 28 | 29 | // GetOrSetDefault returns the value for the given key if it exists. If it does not exist, it creates a default 30 | // with the provided function and sets it for that key 31 | func (m *MutexMap[K, V]) GetOrSetDefault(key K, createDefault func() V) V { 32 | m.Lock() 33 | defer m.Unlock() 34 | 35 | value, ok := m.real[key] 36 | 37 | if !ok { 38 | value = createDefault() 39 | m.real[key] = value 40 | } 41 | 42 | return value 43 | } 44 | 45 | // Has checks if a key exists in the map 46 | func (m *MutexMap[K, V]) Has(key K) bool { 47 | m.RLock() 48 | defer m.RUnlock() 49 | 50 | _, ok := m.real[key] 51 | return ok 52 | } 53 | 54 | // Delete removes a key from the internal map 55 | func (m *MutexMap[K, V]) Delete(key K) { 56 | m.Lock() 57 | defer m.Unlock() 58 | 59 | delete(m.real, key) 60 | } 61 | 62 | // DeleteIf deletes every element if the predicate returns true. 63 | // Returns the amount of elements deleted. 64 | func (m *MutexMap[K, V]) DeleteIf(predicate func(key K, value V) bool) int { 65 | m.Lock() 66 | defer m.Unlock() 67 | 68 | amount := 0 69 | for key, value := range m.real { 70 | if predicate(key, value) { 71 | delete(m.real, key) 72 | amount++ 73 | } 74 | } 75 | 76 | return amount 77 | } 78 | 79 | // RunAndDelete runs a callback and removes the key afterwards 80 | func (m *MutexMap[K, V]) RunAndDelete(key K, callback func(key K, value V)) { 81 | m.Lock() 82 | defer m.Unlock() 83 | 84 | if value, ok := m.real[key]; ok { 85 | callback(key, value) 86 | delete(m.real, key) 87 | } 88 | } 89 | 90 | // Size returns the length of the internal map 91 | func (m *MutexMap[K, V]) Size() int { 92 | m.RLock() 93 | defer m.RUnlock() 94 | 95 | return len(m.real) 96 | } 97 | 98 | // Each runs a callback function for every item in the map 99 | // The map should not be modified inside the callback function 100 | // Returns true if the loop was terminated early 101 | func (m *MutexMap[K, V]) Each(callback func(key K, value V) bool) bool { 102 | m.RLock() 103 | defer m.RUnlock() 104 | 105 | for key, value := range m.real { 106 | if callback(key, value) { 107 | return true 108 | } 109 | } 110 | 111 | return false 112 | } 113 | 114 | // Clear removes all items from the `real` map 115 | // Accepts an optional callback function ran for every item before it is deleted 116 | func (m *MutexMap[K, V]) Clear(callback func(key K, value V)) { 117 | m.Lock() 118 | defer m.Unlock() 119 | 120 | for key, value := range m.real { 121 | if callback != nil { 122 | callback(key, value) 123 | } 124 | delete(m.real, key) 125 | } 126 | } 127 | 128 | // NewMutexMap returns a new instance of MutexMap with the provided key/value types 129 | func NewMutexMap[K comparable, V any]() *MutexMap[K, V] { 130 | return &MutexMap[K, V]{ 131 | RWMutex: &sync.RWMutex{}, 132 | real: make(map[K]V), 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /timeout_manager.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // TimeoutManager is an implementation of rdv::TimeoutManager and manages the resending of reliable PRUDP packets 9 | type TimeoutManager struct { 10 | ctx context.Context 11 | cancel context.CancelFunc 12 | packets *MutexMap[uint16, PRUDPPacketInterface] 13 | streamSettings *StreamSettings 14 | } 15 | 16 | // SchedulePacketTimeout adds a packet to the scheduler and begins it's timer 17 | func (tm *TimeoutManager) SchedulePacketTimeout(packet PRUDPPacketInterface) { 18 | endpoint := packet.Sender().Endpoint().(*PRUDPEndPoint) 19 | 20 | rto := endpoint.ComputeRetransmitTimeout(packet) 21 | ctx, cancel := context.WithTimeout(tm.ctx, rto) 22 | 23 | timeout := NewTimeout() 24 | timeout.SetRTO(rto) 25 | timeout.ctx = ctx 26 | timeout.cancel = cancel 27 | packet.setTimeout(timeout) 28 | 29 | tm.packets.Set(packet.SequenceID(), packet) 30 | go tm.start(packet) 31 | } 32 | 33 | // AcknowledgePacket marks a pending packet as acknowledged. It will be ignored at the next resend attempt 34 | func (tm *TimeoutManager) AcknowledgePacket(sequenceID uint16) { 35 | // * Acknowledge the packet 36 | tm.packets.RunAndDelete(sequenceID, func(_ uint16, packet PRUDPPacketInterface) { 37 | // * Update the RTT on the connection if the packet hasn't been resent 38 | if packet.SendCount() >= tm.streamSettings.RTTRetransmit { 39 | rttm := time.Since(packet.SentAt()) 40 | packet.Sender().(*PRUDPConnection).rtt.Adjust(rttm) 41 | } 42 | }) 43 | } 44 | 45 | func (tm *TimeoutManager) start(packet PRUDPPacketInterface) { 46 | <-packet.getTimeout().ctx.Done() 47 | 48 | connection := packet.Sender().(*PRUDPConnection) 49 | 50 | // * If the connection is closed stop trying to resend 51 | if connection.ConnectionState != StateConnected { 52 | return 53 | } 54 | 55 | if tm.packets.Has(packet.SequenceID()) { 56 | endpoint := packet.Sender().Endpoint().(*PRUDPEndPoint) 57 | 58 | // * This is `<` instead of `<=` for accuracy with observed behavior, even though we're comparing send count vs _resend_ max 59 | if packet.SendCount() < tm.streamSettings.MaxPacketRetransmissions { 60 | packet.incrementSendCount() 61 | packet.setSentAt(time.Now()) 62 | rto := endpoint.ComputeRetransmitTimeout(packet) 63 | 64 | ctx, cancel := context.WithTimeout(tm.ctx, rto) 65 | timeout := packet.getTimeout() 66 | timeout.timeout = rto 67 | timeout.ctx = ctx 68 | timeout.cancel = cancel 69 | 70 | // * Schedule the packet to be resent 71 | go tm.start(packet) 72 | 73 | // * Resend the packet to the connection 74 | server := connection.endpoint.Server 75 | data := packet.Bytes() 76 | server.SendRaw(connection.Socket, data) 77 | } else { 78 | // * Packet has been retried too many times, consider the connection dead 79 | endpoint.CleanupConnection(connection) 80 | } 81 | } 82 | } 83 | 84 | // Stop kills the resend scheduler and stops all pending packets 85 | func (tm *TimeoutManager) Stop() { 86 | tm.cancel() 87 | tm.packets.Clear(func(key uint16, value PRUDPPacketInterface) {}) 88 | } 89 | 90 | // NewTimeoutManager creates a new TimeoutManager 91 | func NewTimeoutManager() *TimeoutManager { 92 | ctx, cancel := context.WithCancel(context.Background()) 93 | return &TimeoutManager{ 94 | ctx: ctx, 95 | cancel: cancel, 96 | packets: NewMutexMap[uint16, PRUDPPacketInterface](), 97 | streamSettings: NewStreamSettings(), 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /constants/signature_method.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // SignatureMethod is an implementation of the nn::nex::PRUDPMessageInterface::SignatureMethod enum. 4 | // 5 | // The signature method is used as part of the packet signature calculation process. It determines 6 | // what data is used and from where when calculating the packets signature. 7 | // 8 | // Currently unused. Implemented for future use and dodumentation/note taking purposes. 9 | // 10 | // The following notes are derived from Xenoblade on the Wii U. Many details are unknown. 11 | // 12 | // Based on the `nn::nex::PRUDPMessageV1::CalcSignatureHelper` (`__CPR210__CalcSignatureHelper__Q3_2nn3nex14PRUDPMessageV1FPCQ3_2nn3nex6PacketQ4_2nn3nex21PRUDPMessageInterface15SignatureMethodPCQ3_2nn3nex3KeyQJ68J3nex6Stream4TypePCQ3_2nn3nex14SignatureBytesRQ3_2nn3nexJ167J`) 13 | // function: 14 | // 15 | // There appears to be 9 signature methods. Methods 0, 2, 3, and 9 seem to do nothing. Method 1 16 | // seems to calculate the signature using the connection address. Methods 4-8 calculate the signature 17 | // using parts of the packet. 18 | // 19 | // - Method 0: Calls `func_0x04b10f90` and bails immediately? 20 | // - Method 1: Seems to calculate the signature using ONLY the connections address? It uses the values 21 | // from `nn::nex::InetAddress::GetAddress` and `nn::nex::InetAddress::GetPortNumber`, among others. 22 | // It does NOT follow the same code path as methods 4-9 23 | // - Method 2: Unknown. Bails without doing anything 24 | // - Method 3: Unknown. Bails without doing anything 25 | // 26 | // Methods 4-8 build the signature from one or many parts of the packet 27 | // 28 | // - Methods 4-8: Use the value from `nn::nex::Packet::GetHeaderForSignatureCalc`? 29 | // - Methods 5-8: Use whatever is passed as `signature_bytes_1`, but only if: 30 | // 1. `signature_bytes_1` is not empty. 31 | // 2. The packet type is not `SYN`. 32 | // 3. The packet type is not `CONNECT`. 33 | // 4. The packet type is not `USER` (?). 34 | // 5. `type_flags & 0x200 == 0`. 35 | // 6. `type_flags & 0x400 == 0`. 36 | // - Method 6: Use an optional "key", if not null 37 | // - If method 7 is used, 2 local variables are set to 0. Otherwise they get set the content pointer 38 | // and size of the calculated signature buffer. In both cases another local variable is set to 39 | // `packet->field_0x94`, and then some checks are done on it before it's set to the packets payload? 40 | // - Method 8: 16 random numbers generated and appended to `signature_bytes_2` 41 | // - Method 9: The signature seems ignored entirely? 42 | type SignatureMethod uint8 43 | 44 | const ( 45 | // SignatureMethod0 is an unknown signature type 46 | SignatureMethod0 SignatureMethod = iota 47 | 48 | // SignatureMethodConnectionAddress seems to indicate the signature is based on the connection address 49 | SignatureMethodConnectionAddress 50 | 51 | // SignatureMethod2 is an unknown signature type 52 | SignatureMethod2 53 | 54 | // SignatureMethod3 is an unknown signature type 55 | SignatureMethod3 56 | 57 | // SignatureMethod4 is an unknown signature method 58 | SignatureMethod4 59 | 60 | // SignatureMethod5 is an unknown signature method 61 | SignatureMethod5 62 | 63 | // SignatureMethodUseKey seems to indicate the signature uses the provided key value, if not null 64 | SignatureMethodUseKey 65 | 66 | // SignatureMethod7 is an unknown signature method 67 | SignatureMethod7 68 | 69 | // SignatureMethodUseEntropy seems to indicate the signature includes 16 random bytes 70 | SignatureMethodUseEntropy 71 | 72 | // SignatureMethodIgnore seems to indicate the signature is ignored 73 | SignatureMethodIgnore 74 | ) 75 | -------------------------------------------------------------------------------- /types/pid.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // PID represents a unique number to identify a user. 11 | // Type alias of uint64. 12 | // The true size of this value depends on the client version. 13 | // Legacy clients (Wii U/3DS) use a uint32, whereas modern clients (Nintendo Switch) use a uint64. 14 | type PID uint64 15 | 16 | // WriteTo writes the PID to the given writable 17 | func (p PID) WriteTo(writable Writable) { 18 | if writable.PIDSize() == 8 { 19 | writable.WriteUInt64LE(uint64(p)) 20 | } else { 21 | writable.WriteUInt32LE(uint32(p)) 22 | } 23 | } 24 | 25 | // ExtractFrom extracts the PID from the given readable 26 | func (p *PID) ExtractFrom(readable Readable) error { 27 | var pid uint64 28 | var err error 29 | 30 | if readable.PIDSize() == 8 { 31 | pid, err = readable.ReadUInt64LE() 32 | } else { 33 | p, e := readable.ReadUInt32LE() 34 | 35 | pid = uint64(p) 36 | err = e 37 | } 38 | 39 | if err != nil { 40 | return err 41 | } 42 | 43 | *p = PID(pid) 44 | return nil 45 | } 46 | 47 | // Copy returns a pointer to a copy of the PID. Requires type assertion when used 48 | func (p PID) Copy() RVType { 49 | return NewPID(uint64(p)) 50 | } 51 | 52 | // Equals checks if the input is equal in value to the current instance 53 | func (p PID) Equals(o RVType) bool { 54 | if _, ok := o.(PID); !ok { 55 | return false 56 | } 57 | 58 | return p == o.(PID) 59 | } 60 | 61 | // CopyRef copies the current value of the PID 62 | // and returns a pointer to the new copy 63 | func (p PID) CopyRef() RVTypePtr { 64 | copied := p.Copy().(PID) 65 | return &copied 66 | } 67 | 68 | // Deref takes a pointer to the PID 69 | // and dereferences it to the raw value. 70 | // Only useful when working with an instance of RVTypePtr 71 | func (p *PID) Deref() RVType { 72 | return *p 73 | } 74 | 75 | // String returns a string representation of the struct 76 | func (p PID) String() string { 77 | return p.FormatToString(0) 78 | } 79 | 80 | // FormatToString pretty-prints the struct data using the provided indentation level 81 | func (p PID) FormatToString(indentationLevel int) string { 82 | indentationValues := strings.Repeat("\t", indentationLevel+1) 83 | indentationEnd := strings.Repeat("\t", indentationLevel) 84 | 85 | var b strings.Builder 86 | 87 | b.WriteString("PID{\n") 88 | b.WriteString(fmt.Sprintf("%spid: %d\n", indentationValues, p)) 89 | b.WriteString(fmt.Sprintf("%s}", indentationEnd)) 90 | 91 | return b.String() 92 | } 93 | 94 | // Scan implements sql.Scanner for database/sql 95 | func (p *PID) Scan(value any) error { 96 | if value == nil { 97 | *p = PID(0) 98 | return nil 99 | } 100 | 101 | switch v := value.(type) { 102 | case int64: // * Postgres might store/return the data as an int64 103 | *p = PID(uint64(v)) 104 | case []byte, string: // * Otherwise, it's stored/returned as a string 105 | var str string 106 | if b, ok := v.([]byte); ok { 107 | str = string(b) 108 | } else { 109 | str = v.(string) 110 | } 111 | 112 | parsed, err := strconv.ParseUint(str, 10, 64) 113 | if err != nil { 114 | return fmt.Errorf("cannot parse string %q into PID: %w", v, err) 115 | } 116 | *p = PID(parsed) 117 | default: 118 | return fmt.Errorf("cannot scan %T into PID", value) 119 | } 120 | 121 | return nil 122 | } 123 | 124 | // Value implements driver.Valuer for database/sql 125 | func (p PID) Value() (driver.Value, error) { 126 | return fmt.Sprintf("%d", uint64(p)), nil 127 | } 128 | 129 | // NewPID returns a PID instance. The real size of PID depends on the client version 130 | func NewPID(input uint64) PID { 131 | p := PID(input) 132 | return p 133 | } 134 | -------------------------------------------------------------------------------- /mutex_slice.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import "sync" 4 | 5 | // TODO - This currently only properly supports Go native types, due to the use of == for comparisons. Can this be updated to support custom types? 6 | 7 | // MutexSlice implements a slice type with go routine safe accessors through mutex locks. 8 | // 9 | // Embeds sync.RWMutex. 10 | type MutexSlice[V comparable] struct { 11 | *sync.RWMutex 12 | real []V 13 | } 14 | 15 | // Add adds a value to the slice 16 | func (m *MutexSlice[V]) Add(value V) { 17 | m.Lock() 18 | defer m.Unlock() 19 | 20 | m.real = append(m.real, value) 21 | } 22 | 23 | // Delete removes the first instance of the given value from the slice. 24 | // 25 | // Returns true if the value existed and was deleted, otherwise returns false. 26 | func (m *MutexSlice[V]) Delete(value V) bool { 27 | m.Lock() 28 | defer m.Unlock() 29 | 30 | for i, v := range m.real { 31 | if v == value { 32 | m.real = append(m.real[:i], m.real[i+1:]...) 33 | return true 34 | } 35 | } 36 | 37 | return false 38 | } 39 | 40 | // DeleteAll removes all instances of the given value from the slice. 41 | // 42 | // Returns true if the value existed and was deleted, otherwise returns false. 43 | func (m *MutexSlice[V]) DeleteAll(value V) bool { 44 | m.Lock() 45 | defer m.Unlock() 46 | 47 | newSlice := make([]V, 0) 48 | oldLength := len(m.real) 49 | 50 | for _, v := range m.real { 51 | if v != value { 52 | newSlice = append(newSlice, v) 53 | } 54 | } 55 | 56 | m.real = newSlice 57 | 58 | return len(newSlice) < oldLength 59 | } 60 | 61 | // Has checks if the slice contains the given value. 62 | func (m *MutexSlice[V]) Has(value V) bool { 63 | m.Lock() 64 | defer m.Unlock() 65 | 66 | for _, v := range m.real { 67 | if v == value { 68 | return true 69 | } 70 | } 71 | 72 | return false 73 | } 74 | 75 | // GetIndex checks if the slice contains the given value and returns it's index. 76 | // 77 | // Returns -1 if the value does not exist in the slice. 78 | func (m *MutexSlice[V]) GetIndex(value V) int { 79 | m.Lock() 80 | defer m.Unlock() 81 | 82 | for i, v := range m.real { 83 | if v == value { 84 | return i 85 | } 86 | } 87 | 88 | return -1 89 | } 90 | 91 | // At returns value at the given index. 92 | // 93 | // Returns a bool indicating if the value was found successfully. 94 | func (m *MutexSlice[V]) At(index int) (V, bool) { 95 | m.Lock() 96 | defer m.Unlock() 97 | 98 | if index >= len(m.real) { 99 | return *new(V), false 100 | } 101 | 102 | return m.real[index], true 103 | } 104 | 105 | // Values returns the internal slice. 106 | func (m *MutexSlice[V]) Values() []V { 107 | m.Lock() 108 | defer m.Unlock() 109 | 110 | return m.real 111 | } 112 | 113 | // Size returns the length of the internal slice 114 | func (m *MutexSlice[V]) Size() int { 115 | m.RLock() 116 | defer m.RUnlock() 117 | 118 | return len(m.real) 119 | } 120 | 121 | // Each runs a callback function for every item in the slice. 122 | // 123 | // The slice cannot be modified inside the callback function. 124 | // 125 | // Returns true if the loop was terminated early. 126 | func (m *MutexSlice[V]) Each(callback func(index int, value V) bool) bool { 127 | m.RLock() 128 | defer m.RUnlock() 129 | 130 | for i, value := range m.real { 131 | if callback(i, value) { 132 | return true 133 | } 134 | } 135 | 136 | return false 137 | } 138 | 139 | // Clear removes all items from the slice. 140 | func (m *MutexSlice[V]) Clear() { 141 | m.Lock() 142 | defer m.Unlock() 143 | 144 | m.real = make([]V, 0) 145 | } 146 | 147 | // NewMutexSlice returns a new instance of MutexSlice with the provided value type 148 | func NewMutexSlice[V comparable]() *MutexSlice[V] { 149 | return &MutexSlice[V]{ 150 | RWMutex: &sync.RWMutex{}, 151 | real: make([]V, 0), 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /types/result_range.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // ResultRange is an implementation of rdv::ResultRange. 9 | // Holds information about how to make queries which may return large data. 10 | type ResultRange struct { 11 | Structure 12 | Offset UInt32 `json:"offset" db:"offset" bson:"offset" xml:"Offset"` // * Offset into the dataset 13 | Length UInt32 `json:"length" db:"length" bson:"length" xml:"Length"` // * Number of items to return 14 | } 15 | 16 | // WriteTo writes the ResultRange to the given writable 17 | func (rr ResultRange) WriteTo(writable Writable) { 18 | contentWritable := writable.CopyNew() 19 | 20 | rr.Offset.WriteTo(contentWritable) 21 | rr.Length.WriteTo(contentWritable) 22 | 23 | content := contentWritable.Bytes() 24 | 25 | rr.WriteHeaderTo(writable, uint32(len(content))) 26 | 27 | writable.Write(content) 28 | } 29 | 30 | // ExtractFrom extracts the ResultRange from the given readable 31 | func (rr *ResultRange) ExtractFrom(readable Readable) error { 32 | var err error 33 | 34 | if err = rr.ExtractHeaderFrom(readable); err != nil { 35 | return fmt.Errorf("Failed to read ResultRange header. %s", err.Error()) 36 | } 37 | 38 | err = rr.Offset.ExtractFrom(readable) 39 | if err != nil { 40 | return fmt.Errorf("Failed to read ResultRange.Offset. %s", err.Error()) 41 | } 42 | 43 | err = rr.Length.ExtractFrom(readable) 44 | if err != nil { 45 | return fmt.Errorf("Failed to read ResultRange.Length. %s", err.Error()) 46 | } 47 | 48 | return nil 49 | } 50 | 51 | // Copy returns a new copied instance of ResultRange 52 | func (rr ResultRange) Copy() RVType { 53 | copied := NewResultRange() 54 | 55 | copied.StructureVersion = rr.StructureVersion 56 | copied.Offset = rr.Offset.Copy().(UInt32) 57 | copied.Length = rr.Length.Copy().(UInt32) 58 | 59 | return copied 60 | } 61 | 62 | // Equals checks if the input is equal in value to the current instance 63 | func (rr ResultRange) Equals(o RVType) bool { 64 | if _, ok := o.(ResultRange); !ok { 65 | return false 66 | } 67 | 68 | other := o.(ResultRange) 69 | 70 | if rr.StructureVersion != other.StructureVersion { 71 | return false 72 | } 73 | 74 | if !rr.Offset.Equals(&other.Offset) { 75 | return false 76 | } 77 | 78 | return rr.Length.Equals(&other.Length) 79 | } 80 | 81 | // CopyRef copies the current value of the ResultRange 82 | // and returns a pointer to the new copy 83 | func (rr ResultRange) CopyRef() RVTypePtr { 84 | copied := rr.Copy().(ResultRange) 85 | return &copied 86 | } 87 | 88 | // Deref takes a pointer to the ResultRange 89 | // and dereferences it to the raw value. 90 | // Only useful when working with an instance of RVTypePtr 91 | func (rr *ResultRange) Deref() RVType { 92 | return *rr 93 | } 94 | 95 | // String returns a string representation of the struct 96 | func (rr ResultRange) String() string { 97 | return rr.FormatToString(0) 98 | } 99 | 100 | // FormatToString pretty-prints the struct data using the provided indentation level 101 | func (rr ResultRange) FormatToString(indentationLevel int) string { 102 | indentationValues := strings.Repeat("\t", indentationLevel+1) 103 | indentationEnd := strings.Repeat("\t", indentationLevel) 104 | 105 | var b strings.Builder 106 | 107 | b.WriteString("ResultRange{\n") 108 | b.WriteString(fmt.Sprintf("%sStructureVersion: %d,\n", indentationValues, rr.StructureVersion)) 109 | b.WriteString(fmt.Sprintf("%sOffset: %s,\n", indentationValues, rr.Offset)) 110 | b.WriteString(fmt.Sprintf("%sLength: %s\n", indentationValues, rr.Length)) 111 | b.WriteString(fmt.Sprintf("%s}", indentationEnd)) 112 | 113 | return b.String() 114 | } 115 | 116 | // NewResultRange returns a new ResultRange 117 | func NewResultRange() ResultRange { 118 | return ResultRange{ 119 | Offset: NewUInt32(0), 120 | Length: NewUInt32(0), 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /byte_stream_out.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import ( 4 | "github.com/PretendoNetwork/nex-go/v2/types" 5 | crunch "github.com/superwhiskers/crunch/v3" 6 | ) 7 | 8 | // ByteStreamOut is an abstraction of github.com/superwhiskers/crunch with nex type support 9 | type ByteStreamOut struct { 10 | *crunch.Buffer 11 | LibraryVersions *LibraryVersions 12 | Settings *ByteStreamSettings 13 | } 14 | 15 | // StringLengthSize returns the expected size of String length fields 16 | func (bso *ByteStreamOut) StringLengthSize() int { 17 | size := 2 18 | 19 | if bso.Settings != nil { 20 | size = bso.Settings.StringLengthSize 21 | } 22 | 23 | return size 24 | } 25 | 26 | // PIDSize returns the size of PID types 27 | func (bso *ByteStreamOut) PIDSize() int { 28 | size := 4 29 | 30 | if bso.Settings != nil { 31 | size = bso.Settings.PIDSize 32 | } 33 | 34 | return size 35 | } 36 | 37 | // UseStructureHeader determines if Structure headers should be used 38 | func (bso *ByteStreamOut) UseStructureHeader() bool { 39 | useStructureHeader := false 40 | 41 | if bso.Settings != nil { 42 | useStructureHeader = bso.Settings.UseStructureHeader 43 | } 44 | 45 | return useStructureHeader 46 | } 47 | 48 | // CopyNew returns a copy of the StreamOut but with a blank internal buffer. Returns as types.Writable 49 | func (bso *ByteStreamOut) CopyNew() types.Writable { 50 | return NewByteStreamOut(bso.LibraryVersions, bso.Settings) 51 | } 52 | 53 | // Writes the input data to the end of the StreamOut 54 | func (bso *ByteStreamOut) Write(data []byte) { 55 | bso.Grow(int64(len(data))) 56 | bso.WriteBytesNext(data) 57 | } 58 | 59 | // WriteUInt8 writes a uint8 60 | func (bso *ByteStreamOut) WriteUInt8(u8 uint8) { 61 | bso.Grow(1) 62 | bso.WriteByteNext(byte(u8)) 63 | } 64 | 65 | // WriteUInt16LE writes a uint16 as LE 66 | func (bso *ByteStreamOut) WriteUInt16LE(u16 uint16) { 67 | bso.Grow(2) 68 | bso.WriteU16LENext([]uint16{u16}) 69 | } 70 | 71 | // WriteUInt32LE writes a uint32 as LE 72 | func (bso *ByteStreamOut) WriteUInt32LE(u32 uint32) { 73 | bso.Grow(4) 74 | bso.WriteU32LENext([]uint32{u32}) 75 | } 76 | 77 | // WriteUInt64LE writes a uint64 as LE 78 | func (bso *ByteStreamOut) WriteUInt64LE(u64 uint64) { 79 | bso.Grow(8) 80 | bso.WriteU64LENext([]uint64{u64}) 81 | } 82 | 83 | // WriteInt8 writes a int8 84 | func (bso *ByteStreamOut) WriteInt8(s8 int8) { 85 | bso.Grow(1) 86 | bso.WriteByteNext(byte(s8)) 87 | } 88 | 89 | // WriteInt16LE writes a uint16 as LE 90 | func (bso *ByteStreamOut) WriteInt16LE(s16 int16) { 91 | bso.Grow(2) 92 | bso.WriteU16LENext([]uint16{uint16(s16)}) 93 | } 94 | 95 | // WriteInt32LE writes a int32 as LE 96 | func (bso *ByteStreamOut) WriteInt32LE(s32 int32) { 97 | bso.Grow(4) 98 | bso.WriteU32LENext([]uint32{uint32(s32)}) 99 | } 100 | 101 | // WriteInt64LE writes a int64 as LE 102 | func (bso *ByteStreamOut) WriteInt64LE(s64 int64) { 103 | bso.Grow(8) 104 | bso.WriteU64LENext([]uint64{uint64(s64)}) 105 | } 106 | 107 | // WriteFloat32LE writes a float32 as LE 108 | func (bso *ByteStreamOut) WriteFloat32LE(f32 float32) { 109 | bso.Grow(4) 110 | bso.WriteF32LENext([]float32{f32}) 111 | } 112 | 113 | // WriteFloat64LE writes a float64 as LE 114 | func (bso *ByteStreamOut) WriteFloat64LE(f64 float64) { 115 | bso.Grow(8) 116 | bso.WriteF64LENext([]float64{f64}) 117 | } 118 | 119 | // WriteBool writes a bool 120 | func (bso *ByteStreamOut) WriteBool(b bool) { 121 | var bVar uint8 122 | if b { 123 | bVar = 1 124 | } 125 | 126 | bso.Grow(1) 127 | bso.WriteByteNext(byte(bVar)) 128 | } 129 | 130 | // NewByteStreamOut returns a new NEX writable byte stream 131 | func NewByteStreamOut(libraryVersions *LibraryVersions, settings *ByteStreamSettings) *ByteStreamOut { 132 | return &ByteStreamOut{ 133 | Buffer: crunch.NewBuffer(), 134 | LibraryVersions: libraryVersions, 135 | Settings: settings, 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /test/hpp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/PretendoNetwork/nex-go/v2" 7 | "github.com/PretendoNetwork/nex-go/v2/types" 8 | ) 9 | 10 | var hppServer *nex.HPPServer 11 | 12 | // * Took these structs out of the protocols lib for convenience 13 | 14 | type dataStoreGetNotificationURLParam struct { 15 | types.Structure 16 | PreviousURL types.String 17 | } 18 | 19 | func (d *dataStoreGetNotificationURLParam) ExtractFrom(readable types.Readable) error { 20 | var err error 21 | 22 | if err = d.ExtractHeaderFrom(readable); err != nil { 23 | return fmt.Errorf("Failed to extract DataStoreGetNotificationURLParam header. %s", err.Error()) 24 | } 25 | 26 | err = d.PreviousURL.ExtractFrom(readable) 27 | if err != nil { 28 | return fmt.Errorf("Failed to extract DataStoreGetNotificationURLParam.PreviousURL. %s", err.Error()) 29 | } 30 | 31 | return nil 32 | } 33 | 34 | type dataStoreReqGetNotificationURLInfo struct { 35 | types.Structure 36 | URL types.String 37 | Key types.String 38 | Query types.String 39 | RootCACert types.Buffer 40 | } 41 | 42 | func (d *dataStoreReqGetNotificationURLInfo) WriteTo(writable types.Writable) { 43 | contentWritable := writable.CopyNew() 44 | 45 | d.URL.WriteTo(contentWritable) 46 | d.Key.WriteTo(contentWritable) 47 | d.Query.WriteTo(contentWritable) 48 | d.RootCACert.WriteTo(contentWritable) 49 | 50 | content := contentWritable.Bytes() 51 | 52 | d.WriteHeaderTo(writable, uint32(len(content))) 53 | 54 | writable.Write(content) 55 | } 56 | 57 | func passwordFromPID(pid *types.PID) (string, uint32) { 58 | return "notmypassword", 0 59 | } 60 | 61 | func startHPPServer() { 62 | fmt.Println("Starting HPP") 63 | 64 | hppServer = nex.NewHPPServer() 65 | 66 | hppServer.OnData(func(packet nex.PacketInterface) { 67 | if packet, ok := packet.(*nex.HPPPacket); ok { 68 | request := packet.RMCMessage() 69 | 70 | fmt.Println("[HPP]", request.ProtocolID, request.MethodID) 71 | 72 | if request.ProtocolID == 0x73 { // * DataStore 73 | if request.MethodID == 0xD { 74 | getNotificationURL(packet) 75 | } 76 | } 77 | } 78 | }) 79 | 80 | hppServer.LibraryVersions().SetDefault(nex.NewLibraryVersion(2, 4, 1)) 81 | hppServer.SetAccessKey("76f26496") 82 | hppServer.AccountDetailsByPID = accountDetailsByPID 83 | hppServer.AccountDetailsByUsername = accountDetailsByUsername 84 | 85 | hppServer.Listen(12345) 86 | } 87 | 88 | func getNotificationURL(packet *nex.HPPPacket) { 89 | request := packet.RMCMessage() 90 | response := nex.NewRMCMessage(hppServer) 91 | 92 | parameters := request.Parameters 93 | 94 | parametersStream := nex.NewByteStreamIn(parameters, hppServer.LibraryVersions(), hppServer.ByteStreamSettings()) 95 | 96 | param := &dataStoreGetNotificationURLParam{} 97 | param.PreviousURL = types.NewString("") 98 | err := param.ExtractFrom(parametersStream) 99 | if err != nil { 100 | fmt.Println("[HPP]", err) 101 | return 102 | } 103 | 104 | fmt.Println("[HPP]", param.PreviousURL) 105 | 106 | responseStream := nex.NewByteStreamOut(hppServer.LibraryVersions(), hppServer.ByteStreamSettings()) 107 | 108 | info := &dataStoreReqGetNotificationURLInfo{} 109 | info.URL = types.NewString("https://example.com") 110 | info.Key = types.NewString("whatever/key") 111 | info.Query = types.NewString("?pretendo=1") 112 | info.RootCACert = types.NewBuffer(nil) 113 | 114 | info.WriteTo(responseStream) 115 | 116 | response.IsSuccess = true 117 | response.IsRequest = false 118 | response.ErrorCode = 0x00010001 119 | response.ProtocolID = request.ProtocolID 120 | response.CallID = request.CallID 121 | response.MethodID = request.MethodID 122 | response.Parameters = responseStream.Bytes() 123 | 124 | // * We replace the RMC message so that it can be delivered back 125 | packet.SetRMCMessage(response) 126 | 127 | hppServer.Send(packet) 128 | } 129 | -------------------------------------------------------------------------------- /types/qresult.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | var errorMask = 1 << 31 11 | 12 | // QResult is an implementation of rdv::qResult. 13 | // Type alias of uint32. 14 | // Determines the result of an operation. 15 | // If the MSB is set the result is an error, otherwise success 16 | type QResult uint32 17 | 18 | // WriteTo writes the QResult to the given writable 19 | func (r QResult) WriteTo(writable Writable) { 20 | writable.WriteUInt32LE(uint32(r)) 21 | } 22 | 23 | // ExtractFrom extracts the QResult from the given readable 24 | func (r *QResult) ExtractFrom(readable Readable) error { 25 | code, err := readable.ReadUInt32LE() 26 | if err != nil { 27 | return fmt.Errorf("Failed to read QResult code. %s", err.Error()) 28 | } 29 | 30 | *r = QResult(code) 31 | return nil 32 | } 33 | 34 | // Copy returns a pointer to a copy of the QResult. Requires type assertion when used 35 | func (r QResult) Copy() RVType { 36 | return NewQResult(uint32(r)) 37 | } 38 | 39 | // Equals checks if the input is equal in value to the current instance 40 | func (r QResult) Equals(o RVType) bool { 41 | if _, ok := o.(QResult); !ok { 42 | return false 43 | } 44 | 45 | return r == o.(QResult) 46 | } 47 | 48 | // CopyRef copies the current value of the QResult 49 | // and returns a pointer to the new copy 50 | func (r QResult) CopyRef() RVTypePtr { 51 | copied := r.Copy().(QResult) 52 | return &copied 53 | } 54 | 55 | // Deref takes a pointer to the QResult 56 | // and dereferences it to the raw value. 57 | // Only useful when working with an instance of RVTypePtr 58 | func (r *QResult) Deref() RVType { 59 | return *r 60 | } 61 | 62 | // IsSuccess returns true if the QResult is a success 63 | func (r QResult) IsSuccess() bool { 64 | return int(r)&errorMask == 0 65 | } 66 | 67 | // IsError returns true if the QResult is a error 68 | func (r QResult) IsError() bool { 69 | return int(r)&errorMask != 0 70 | } 71 | 72 | // String returns a string representation of the struct 73 | func (r QResult) String() string { 74 | return r.FormatToString(0) 75 | } 76 | 77 | // FormatToString pretty-prints the struct data using the provided indentation level 78 | func (r QResult) FormatToString(indentationLevel int) string { 79 | indentationValues := strings.Repeat("\t", indentationLevel+1) 80 | indentationEnd := strings.Repeat("\t", indentationLevel) 81 | 82 | var b strings.Builder 83 | 84 | b.WriteString("QResult{\n") 85 | 86 | if r.IsSuccess() { 87 | b.WriteString(fmt.Sprintf("%scode: %d (success)\n", indentationValues, r)) 88 | } else { 89 | b.WriteString(fmt.Sprintf("%scode: %d (error)\n", indentationValues, r)) 90 | } 91 | 92 | b.WriteString(fmt.Sprintf("%s}", indentationEnd)) 93 | 94 | return b.String() 95 | } 96 | 97 | // Scan implements sql.Scanner for database/sql 98 | func (r *QResult) Scan(value any) error { 99 | if value == nil { 100 | *r = QResult(0) 101 | return nil 102 | } 103 | 104 | switch v := value.(type) { 105 | case int64: 106 | *r = QResult(v) 107 | case string: 108 | parsed, err := strconv.ParseUint(v, 10, 32) 109 | if err != nil { 110 | return fmt.Errorf("cannot parse string %q into QResult: %w", v, err) 111 | } 112 | *r = QResult(parsed) 113 | default: 114 | return fmt.Errorf("cannot scan %T into QResult", value) 115 | } 116 | 117 | return nil 118 | } 119 | 120 | // Value implements driver.Valuer for database/sql 121 | func (r QResult) Value() (driver.Value, error) { 122 | return int64(r), nil 123 | } 124 | 125 | // NewQResult returns a new QResult 126 | func NewQResult(input uint32) QResult { 127 | r := QResult(input) 128 | return r 129 | } 130 | 131 | // NewQResultSuccess returns a new QResult set as a success 132 | func NewQResultSuccess(code uint32) QResult { 133 | return NewQResult(uint32(int(code) & ^errorMask)) 134 | } 135 | 136 | // NewQResultError returns a new QResult set as an error 137 | func NewQResultError(code uint32) QResult { 138 | return NewQResult(uint32(int(code) | errorMask)) 139 | } 140 | -------------------------------------------------------------------------------- /types/variant.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // VariantTypes holds a mapping of RVTypes that are accessible in a Variant 9 | var VariantTypes = make(map[UInt8]RVType) 10 | 11 | // RegisterVariantType registers a RVType to be accessible in a Variant 12 | func RegisterVariantType(id UInt8, rvType RVType) { 13 | VariantTypes[id] = rvType 14 | } 15 | 16 | // Variant is an implementation of rdv::Variant. 17 | // This type can hold many other types, denoted by a type ID. 18 | type Variant struct { 19 | TypeID UInt8 `json:"type_id" db:"type_id" bson:"type_id" xml:"TypeID"` 20 | Type RVType `json:"type" db:"type" bson:"type" xml:"Type"` 21 | } 22 | 23 | // WriteTo writes the Variant to the given writable 24 | func (v Variant) WriteTo(writable Writable) { 25 | v.TypeID.WriteTo(writable) 26 | 27 | if v.Type != nil { 28 | v.Type.WriteTo(writable) 29 | } 30 | } 31 | 32 | // ExtractFrom extracts the Variant from the given readable 33 | func (v *Variant) ExtractFrom(readable Readable) error { 34 | err := v.TypeID.ExtractFrom(readable) 35 | if err != nil { 36 | return fmt.Errorf("Failed to read Variant type ID. %s", err.Error()) 37 | } 38 | 39 | typeID := v.TypeID 40 | 41 | // * Type ID of 0 is a "None" type. There is no data 42 | if typeID == 0 { 43 | return nil 44 | } 45 | 46 | if _, ok := VariantTypes[typeID]; !ok { 47 | return fmt.Errorf("Invalid Variant type ID %d", typeID) 48 | } 49 | 50 | // * Create a new copy and get a pointer to it. 51 | // * Required so that we have access to ExtractFrom 52 | ptr := VariantTypes[typeID].CopyRef() 53 | if err := ptr.ExtractFrom(readable); err != nil { 54 | return fmt.Errorf("Failed to read Variant type data. %s", err.Error()) 55 | } 56 | 57 | v.Type = ptr.Deref() // * Dereference the RVTypePtr pointer back into a non-pointer type 58 | 59 | return nil 60 | } 61 | 62 | // Copy returns a pointer to a copy of the Variant. Requires type assertion when used 63 | func (v Variant) Copy() RVType { 64 | copied := NewVariant() 65 | 66 | copied.TypeID = v.TypeID.Copy().(UInt8) 67 | 68 | if v.Type != nil { 69 | copied.Type = v.Type.Copy() 70 | } 71 | 72 | return copied 73 | } 74 | 75 | // Equals checks if the input is equal in value to the current instance 76 | func (v Variant) Equals(o RVType) bool { 77 | if _, ok := o.(Variant); !ok { 78 | return false 79 | } 80 | 81 | other := o.(Variant) 82 | 83 | if !v.TypeID.Equals(other.TypeID) { 84 | return false 85 | } 86 | 87 | if v.Type != nil { 88 | return v.Type.Equals(other.Type) 89 | } 90 | 91 | return true 92 | } 93 | 94 | // CopyRef copies the current value of the Variant 95 | // and returns a pointer to the new copy 96 | func (v Variant) CopyRef() RVTypePtr { 97 | copied := v.Copy().(Variant) 98 | return &copied 99 | } 100 | 101 | // Deref takes a pointer to the Variant 102 | // and dereferences it to the raw value. 103 | // Only useful when working with an instance of RVTypePtr 104 | func (v *Variant) Deref() RVType { 105 | return *v 106 | } 107 | 108 | // String returns a string representation of the struct 109 | func (v Variant) String() string { 110 | return v.FormatToString(0) 111 | } 112 | 113 | // FormatToString pretty-prints the struct data using the provided indentation level 114 | func (v Variant) FormatToString(indentationLevel int) string { 115 | indentationValues := strings.Repeat("\t", indentationLevel+1) 116 | indentationEnd := strings.Repeat("\t", indentationLevel) 117 | 118 | var b strings.Builder 119 | 120 | b.WriteString("Variant{\n") 121 | b.WriteString(fmt.Sprintf("%sTypeID: %s,\n", indentationValues, v.TypeID)) 122 | 123 | if v.Type != nil { 124 | b.WriteString(fmt.Sprintf("%sType: %s\n", indentationValues, v.Type)) 125 | } else { 126 | b.WriteString(fmt.Sprintf("%sType: None\n", indentationValues)) 127 | } 128 | 129 | b.WriteString(fmt.Sprintf("%s}", indentationEnd)) 130 | 131 | return b.String() 132 | } 133 | 134 | // NewVariant returns a new Variant 135 | func NewVariant() Variant { 136 | // * Type ID of 0 is a "None" type. There is no data 137 | return Variant{ 138 | TypeID: NewUInt8(0), 139 | Type: nil, 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /hpp_packet.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/md5" 7 | "encoding/hex" 8 | "errors" 9 | "fmt" 10 | ) 11 | 12 | // HPPPacket holds all the data about an HPP request 13 | type HPPPacket struct { 14 | sender *HPPClient 15 | accessKeySignature []byte 16 | passwordSignature []byte 17 | payload []byte 18 | message *RMCMessage 19 | processed chan bool 20 | } 21 | 22 | // Sender returns the Client who sent the packet 23 | func (p *HPPPacket) Sender() ConnectionInterface { 24 | return p.sender 25 | } 26 | 27 | // Payload returns the packets payload 28 | func (p *HPPPacket) Payload() []byte { 29 | return p.payload 30 | } 31 | 32 | // SetPayload sets the packets payload 33 | func (p *HPPPacket) SetPayload(payload []byte) { 34 | p.payload = payload 35 | } 36 | 37 | func (p *HPPPacket) validateAccessKeySignature(signature string) error { 38 | signatureBytes, err := hex.DecodeString(signature) 39 | if err != nil { 40 | return fmt.Errorf("Failed to decode access key signature. %s", err) 41 | } 42 | 43 | p.accessKeySignature = signatureBytes 44 | 45 | calculatedSignature, err := p.calculateAccessKeySignature() 46 | if err != nil { 47 | return fmt.Errorf("Failed to calculate access key signature. %s", err) 48 | } 49 | 50 | if !bytes.Equal(calculatedSignature, p.accessKeySignature) { 51 | return errors.New("Access key signature does not match") 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func (p *HPPPacket) calculateAccessKeySignature() ([]byte, error) { 58 | accessKey := p.Sender().Endpoint().AccessKey() 59 | 60 | accessKeyBytes, err := hex.DecodeString(accessKey) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | signature, err := p.calculateSignature(p.payload, accessKeyBytes) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return signature, nil 71 | } 72 | 73 | func (p *HPPPacket) validatePasswordSignature(signature string) error { 74 | signatureBytes, err := hex.DecodeString(signature) 75 | if err != nil { 76 | return fmt.Errorf("Failed to decode password signature. %s", err) 77 | } 78 | 79 | p.passwordSignature = signatureBytes 80 | 81 | calculatedSignature, err := p.calculatePasswordSignature() 82 | if err != nil { 83 | return fmt.Errorf("Failed to calculate password signature. %s", err) 84 | } 85 | 86 | if !bytes.Equal(calculatedSignature, p.passwordSignature) { 87 | return errors.New("Password signature does not match") 88 | } 89 | 90 | return nil 91 | } 92 | 93 | func (p *HPPPacket) calculatePasswordSignature() ([]byte, error) { 94 | sender := p.Sender() 95 | pid := sender.PID() 96 | account, _ := sender.Endpoint().(*HPPServer).AccountDetailsByPID(pid) 97 | if account == nil { 98 | return nil, errors.New("PID does not exist") 99 | } 100 | 101 | key := DeriveKerberosKey(pid, []byte(account.Password)) 102 | 103 | signature, err := p.calculateSignature(p.payload, key) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | return signature, nil 109 | } 110 | 111 | func (p *HPPPacket) calculateSignature(buffer []byte, key []byte) ([]byte, error) { 112 | mac := hmac.New(md5.New, key) 113 | 114 | _, err := mac.Write(buffer) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | hmac := mac.Sum(nil) 120 | 121 | return hmac, nil 122 | } 123 | 124 | // RMCMessage returns the packets RMC Message 125 | func (p *HPPPacket) RMCMessage() *RMCMessage { 126 | return p.message 127 | } 128 | 129 | // SetRMCMessage sets the packets RMC Message 130 | func (p *HPPPacket) SetRMCMessage(message *RMCMessage) { 131 | p.message = message 132 | } 133 | 134 | // NewHPPPacket creates and returns a new HPPPacket using the provided Client and payload 135 | func NewHPPPacket(client *HPPClient, payload []byte) (*HPPPacket, error) { 136 | hppPacket := &HPPPacket{ 137 | sender: client, 138 | payload: payload, 139 | processed: make(chan bool), 140 | } 141 | 142 | if payload != nil { 143 | rmcMessage := NewRMCRequest(client.Endpoint()) 144 | err := rmcMessage.FromBytes(payload) 145 | if err != nil { 146 | return nil, fmt.Errorf("Failed to decode HPP request. %s", err) 147 | } 148 | 149 | hppPacket.SetRMCMessage(rmcMessage) 150 | } 151 | 152 | return hppPacket, nil 153 | } 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NEX Go 2 | 3 | [![GoDoc](https://godoc.org/github.com/PretendoNetwork/nex-go?status.svg)](https://godoc.org/github.com/PretendoNetwork/nex-go) 4 | 5 | ### Overview 6 | NEX is the networking library used by all 1st party, and many 3rd party, games on the Nintendo Wii U, 3DS, and Switch which have online features. The NEX library has many different parts, ranging from low level packet transport to higher level service implementations 7 | 8 | This library implements the lowest level parts of NEX, the transport protocols. For other parts of the NEX stack, see the below libraries. For detailed information on NEX as a whole, see our wiki docs https://nintendo-wiki.pretendo.network/docs/nex 9 | 10 | ### Install 11 | 12 | ``` 13 | go get github.com/PretendoNetwork/nex-go/v2 14 | ``` 15 | 16 | ### Other NEX libraries 17 | - [nex-protocols-go](https://github.com/PretendoNetwork/nex-protocols-go) - NEX protocol definitions 18 | - [nex-protocols-common-go](https://github.com/PretendoNetwork/nex-protocols-common-go) - Implementations of common NEX protocols which can be reused on many servers 19 | 20 | ### Quazal Rendez-Vous 21 | Nintendo did not make NEX from scratch. NEX is largely based on an existing library called Rendez-Vous (QRV), made by Canadian software company Quazal. Quazal licensed Rendez-Vous out to many other companies, and was eventually bought out by Ubisoft. Because of this, QRV is seen in many many other games on all major platforms, especially Ubisoft 22 | 23 | Nintendo modified Rendez-Vous somewhat heavily, simplifying the library/transport protocol quite a bit, and adding several custom services 24 | 25 | While the main goal of this library is to support games which use the NEX variant of Rendez-Vous made by Nintendo, we also aim to be compatible with games using the original Rendez-Vous library. Due to the extensible nature of Rendez-Vous, many games may feature customizations much like NEX and have non-standard features/behavior. We do our best to support these cases, but there may be times where supporting all variations becomes untenable. In those cases, a fork of these libraries should be made instead if they require heavy modifications 26 | 27 | ### Supported features 28 | - [x] Quazal compatibility mode/settings 29 | - [x] [HPP servers](https://nintendo-wiki.pretendo.network/docs/hpp) (NEX over HTTP) 30 | - [x] [PRUDP servers](https://nintendo-wiki.pretendo.network/docs/prudp) 31 | - [x] UDP transport 32 | - [x] WebSocket transport (Experimental, largely untested) 33 | - [x] PRUDPv0 packets 34 | - [x] PRUDPv1 packets 35 | - [x] PRUDPLite packets 36 | - [x] Fragmented packet payloads 37 | - [x] Packet retransmission 38 | - [x] Reliable packets 39 | - [x] Unreliable packets 40 | - [x] [Virtual ports](https://nintendo-wiki.pretendo.network/docs/prudp#virtual-ports) 41 | - [x] Packet compression 42 | - [x] [RMC](https://nintendo-wiki.pretendo.network/docs/rmc) 43 | - [x] Request messages 44 | - [x] Response messages 45 | - [x] "Packed" encoded messages 46 | - [x] "Packed" (extended) encoded messages 47 | - [x] "Verbose" encoded messages 48 | - [x] [Kerberos authentication](https://nintendo-wiki.pretendo.network/docs/nex/kerberos) 49 | 50 | ### Example 51 | 52 | ```go 53 | package main 54 | 55 | import ( 56 | "fmt" 57 | 58 | "github.com/PretendoNetwork/nex-go/v2" 59 | "github.com/PretendoNetwork/nex-go/v2/types" 60 | ) 61 | 62 | func main() { 63 | // Skeleton of a WiiU/3DS Friends server running on PRUDPv0 with a single endpoint 64 | 65 | authServer := nex.NewPRUDPServer() // The main PRUDP server 66 | endpoint := nex.NewPRUDPEndPoint(1) // A PRUDP endpoint for PRUDP connections to connect to. Bound to StreamID 1 67 | endpoint.ServerAccount = nex.NewAccount(types.NewPID(1), "Quazal Authentication", "password")) 68 | endpoint.AccountDetailsByPID = accountDetailsByPID 69 | endpoint.AccountDetailsByUsername = accountDetailsByUsername 70 | 71 | // Setup event handlers for the endpoint 72 | endpoint.OnData(func(packet nex.PacketInterface) { 73 | if packet, ok := packet.(nex.PRUDPPacketInterface); ok { 74 | request := packet.RMCMessage() 75 | 76 | fmt.Println("[AUTH]", request.ProtocolID, request.MethodID) 77 | 78 | if request.ProtocolID == 0xA { // TicketGrantingProtocol 79 | if request.MethodID == 0x1 { // TicketGrantingProtocol::Login 80 | handleLogin(packet) 81 | } 82 | 83 | if request.MethodID == 0x3 { // TicketGrantingProtocol::RequestTicket 84 | handleRequestTicket(packet) 85 | } 86 | } 87 | } 88 | }) 89 | 90 | // Bind the endpoint to the server and configure it's settings 91 | authServer.BindPRUDPEndPoint(endpoint) 92 | authServer.SetFragmentSize(962) 93 | authServer.LibraryVersions.SetDefault(nex.NewLibraryVersion(1, 1, 0)) 94 | authServer.SessionKeyLength = 16 95 | authServer.AccessKey = "ridfebb9" 96 | authServer.Listen(60000) 97 | } 98 | ``` 99 | -------------------------------------------------------------------------------- /stream_settings.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import ( 4 | "github.com/PretendoNetwork/nex-go/v2/compression" 5 | "github.com/PretendoNetwork/nex-go/v2/encryption" 6 | ) 7 | 8 | // StreamSettings is an implementation of rdv::StreamSettings. 9 | // StreamSettings holds the state and settings for a PRUDP virtual connection stream. 10 | // Each virtual connection is composed of a virtual port and stream type. 11 | // In the original library this would be tied to a rdv::Stream class, but here it is not. 12 | // The original library has more settings which are not present here as their use is unknown. 13 | // Not all values are used at this time, and only exist to future-proof for a later time. 14 | type StreamSettings struct { 15 | ExtraRetransmitTimeoutTrigger uint32 // * The number of times a packet can be retransmitted before ExtraRetransmitTimeoutMultiplier is used 16 | MaxPacketRetransmissions uint32 // * The number of times a packet can be retransmitted before the timeout time is checked 17 | KeepAliveTimeout uint32 // * Presumably the time a packet can be alive for without acknowledgement? Milliseconds? 18 | ChecksumBase uint32 // * Unused. The base value for PRUDPv0 checksum calculations 19 | FaultDetectionEnabled bool // * Unused. Presumably used to detect PIA faults? 20 | InitialRTT uint32 // * The initial connection RTT used for all non-SYN packets 21 | SynInitialRTT uint32 // * The initial connection RTT used for all SYN packets 22 | EncryptionAlgorithm encryption.Algorithm // * The encryption algorithm used for packet payloads 23 | ExtraRetransmitTimeoutMultiplier float32 // * Used as part of the RTO calculations when retransmitting a packet. Only used if ExtraRestransmitTimeoutTrigger has been reached 24 | WindowSize uint32 // * Unused. The max number of (reliable?) packets allowed in a SlidingWindow 25 | CompressionAlgorithm compression.Algorithm // * The compression algorithm used for packet payloads 26 | RTTRetransmit uint32 // * This is the number of times that a retried packet will be included in RTT calculations if we receive an ACK packet for it 27 | RetransmitTimeoutMultiplier float32 // * Used as part of the RTO calculations when retransmitting a packet. Only used if ExtraRestransmitTimeoutTrigger has not been reached 28 | MaxSilenceTime uint32 // * Presumably the time a connection can go without any packets from the other side? Milliseconds? 29 | } 30 | 31 | // Copy returns a new copy of the settings 32 | func (ss *StreamSettings) Copy() *StreamSettings { 33 | copied := NewStreamSettings() 34 | 35 | copied.ExtraRetransmitTimeoutTrigger = ss.ExtraRetransmitTimeoutTrigger 36 | copied.MaxPacketRetransmissions = ss.MaxPacketRetransmissions 37 | copied.KeepAliveTimeout = ss.KeepAliveTimeout 38 | copied.ChecksumBase = ss.ChecksumBase 39 | copied.FaultDetectionEnabled = ss.FaultDetectionEnabled 40 | copied.InitialRTT = ss.InitialRTT 41 | copied.EncryptionAlgorithm = ss.EncryptionAlgorithm.Copy() 42 | copied.ExtraRetransmitTimeoutMultiplier = ss.ExtraRetransmitTimeoutMultiplier 43 | copied.WindowSize = ss.WindowSize 44 | copied.CompressionAlgorithm = ss.CompressionAlgorithm.Copy() 45 | copied.RTTRetransmit = ss.RTTRetransmit 46 | copied.RetransmitTimeoutMultiplier = ss.RetransmitTimeoutMultiplier 47 | copied.MaxSilenceTime = ss.MaxSilenceTime 48 | 49 | return copied 50 | } 51 | 52 | // NewStreamSettings returns a new instance of StreamSettings with default params 53 | func NewStreamSettings() *StreamSettings { 54 | // * Default values based on WATCH_DOGS other than where stated. Not all values are used currently, and only 55 | // * exist to mimic what is seen in that game. Many are planned for future use. 56 | return &StreamSettings{ 57 | ExtraRetransmitTimeoutTrigger: 0x32, 58 | MaxPacketRetransmissions: 0x14, 59 | KeepAliveTimeout: 1000, 60 | ChecksumBase: 0, 61 | FaultDetectionEnabled: true, 62 | InitialRTT: 0x2EE, 63 | SynInitialRTT: 0xFA, 64 | EncryptionAlgorithm: encryption.NewRC4Encryption(), 65 | ExtraRetransmitTimeoutMultiplier: 1.0, 66 | WindowSize: 8, 67 | CompressionAlgorithm: compression.NewDummyCompression(), 68 | RTTRetransmit: 2, // * This value is taken from Xenoblade Chronicles, WATCH_DOGS sets this to 0x32 but it is then ignored. Setting this to 2 matches the TCP spec by not using resent packets in RTT calculations. 69 | RetransmitTimeoutMultiplier: 1.25, 70 | MaxSilenceTime: 10000, // * This value is taken from Xenoblade Chronicles, WATCH_DOGS sets this to 5000. 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /types/any_object_holder.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // HoldableObject defines a common interface for types which can be placed in AnyObjectHolder 9 | type HoldableObject interface { 10 | RVType 11 | ObjectID() RVType // Returns the object identifier of the type 12 | } 13 | 14 | // AnyObjectHolderObjects holds a mapping of RVTypes that are accessible in a AnyDataHolder 15 | var AnyObjectHolderObjects = make(map[RVType]HoldableObject) 16 | 17 | // RegisterObjectHolderType registers a RVType to be accessible in a AnyDataHolder 18 | func RegisterObjectHolderType(rvType HoldableObject) { 19 | AnyObjectHolderObjects[rvType.ObjectID()] = rvType 20 | } 21 | 22 | // AnyObjectHolder can hold a reference to any RVType which can be held 23 | type AnyObjectHolder[T HoldableObject] struct { 24 | Object T 25 | } 26 | 27 | // WriteTo writes the AnyObjectHolder to the given writable 28 | func (aoh AnyObjectHolder[T]) WriteTo(writable Writable) { 29 | contentWritable := writable.CopyNew() 30 | 31 | aoh.Object.WriteTo(contentWritable) 32 | 33 | objectBuffer := NewBuffer(contentWritable.Bytes()) 34 | 35 | objectBufferLength := uint32(len(objectBuffer) + 4) // * Length of the Buffer 36 | 37 | aoh.Object.ObjectID().WriteTo(writable) 38 | writable.WriteUInt32LE(objectBufferLength) 39 | objectBuffer.WriteTo(writable) 40 | } 41 | 42 | // ExtractFrom extracts the AnyObjectHolder from the given readable 43 | func (aoh *AnyObjectHolder[T]) ExtractFrom(readable Readable) error { 44 | var err error 45 | 46 | // TODO - This assumes the identifier is a String 47 | identifier := NewString("") 48 | err = identifier.ExtractFrom(readable) 49 | if err != nil { 50 | return fmt.Errorf("Failed to read AnyObjectHolder identifier. %s", err.Error()) 51 | } 52 | 53 | length := NewUInt32(0) 54 | err = length.ExtractFrom(readable) 55 | if err != nil { 56 | return fmt.Errorf("Failed to read AnyObjectHolder length. %s", err.Error()) 57 | } 58 | 59 | // * This is technically a Buffer, but we can't instantiate a new Readable from here so interpret it as a UInt32 and the object data 60 | bufferLength := NewUInt32(0) 61 | err = bufferLength.ExtractFrom(readable) 62 | if err != nil { 63 | return fmt.Errorf("Failed to read AnyObjectHolder buffer length. %s", err.Error()) 64 | } 65 | 66 | if _, ok := AnyObjectHolderObjects[identifier]; !ok { 67 | return fmt.Errorf("Unknown AnyObjectHolder identifier: %s", identifier) 68 | } 69 | 70 | ptr := AnyObjectHolderObjects[identifier].CopyRef() 71 | 72 | if err := ptr.ExtractFrom(readable); err != nil { 73 | return fmt.Errorf("Failed to read AnyObjectHolder object. %s", err.Error()) 74 | } 75 | 76 | var ok bool 77 | if aoh.Object, ok = ptr.Deref().(T); !ok { 78 | return fmt.Errorf("Input AnyObjectHolder object %s is invalid", identifier) 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // Copy returns a new copied instance of AnyObjectHolder 85 | func (aoh AnyObjectHolder[T]) Copy() RVType { 86 | copied := NewAnyObjectHolder[T]() 87 | 88 | copied.Object = aoh.Object.Copy().(T) 89 | 90 | return copied 91 | } 92 | 93 | // Equals checks if the passed Structure contains the same data as the current instance 94 | func (aoh AnyObjectHolder[T]) Equals(o RVType) bool { 95 | if _, ok := o.(AnyObjectHolder[T]); !ok { 96 | return false 97 | } 98 | 99 | other := o.(AnyObjectHolder[T]) 100 | 101 | return aoh.Object.Equals(other.Object) 102 | } 103 | 104 | // CopyRef copies the current value of the AnyObjectHolder 105 | // and returns a pointer to the new copy 106 | func (aoh AnyObjectHolder[T]) CopyRef() RVTypePtr { 107 | copied := aoh.Copy().(AnyObjectHolder[T]) 108 | return &copied 109 | } 110 | 111 | // Deref takes a pointer to the AnyObjectHolder 112 | // and dereferences it to the raw value. 113 | // Only useful when working with an instance of RVTypePtr 114 | func (aoh *AnyObjectHolder[T]) Deref() RVType { 115 | return *aoh 116 | } 117 | 118 | // String returns a string representation of the struct 119 | func (aoh AnyObjectHolder[T]) String() string { 120 | return aoh.FormatToString(0) 121 | } 122 | 123 | // FormatToString pretty-prints the struct data using the provided indentation level 124 | func (aoh AnyObjectHolder[T]) FormatToString(indentationLevel int) string { 125 | indentationValues := strings.Repeat("\t", indentationLevel+1) 126 | indentationEnd := strings.Repeat("\t", indentationLevel) 127 | 128 | var b strings.Builder 129 | 130 | b.WriteString("AnyDataHolder{\n") 131 | b.WriteString(fmt.Sprintf("%sIdentifier: %s,\n", indentationValues, aoh.Object.ObjectID())) 132 | b.WriteString(fmt.Sprintf("%sObject: %s\n", indentationValues, aoh.Object)) 133 | 134 | b.WriteString(fmt.Sprintf("%s}", indentationEnd)) 135 | 136 | return b.String() 137 | } 138 | 139 | // NewAnyObjectHolder returns a new AnyObjectHolder 140 | func NewAnyObjectHolder[T HoldableObject]() AnyObjectHolder[T] { 141 | return AnyObjectHolder[T]{} 142 | } 143 | -------------------------------------------------------------------------------- /types/map.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Map represents a Quazal Rendez-Vous/NEX Map type. 9 | // Type alias of map[K]V. 10 | // There is not an official type in either the rdv or nn::nex namespaces. 11 | // May have any RVType as both a key and value. If either they key or 12 | // value types are not an RVType, they are ignored. 13 | // 14 | // Incompatible with RVType pointers! 15 | type Map[K comparable, V RVType] map[K]V 16 | 17 | func (m Map[K, V]) writeType(t any, writable Writable) { 18 | // * This just makes Map.WriteTo() a bit cleaner 19 | // * since it doesn't have to type check 20 | if rvt, ok := t.(interface{ WriteTo(writable Writable) }); ok { 21 | rvt.WriteTo(writable) 22 | } 23 | } 24 | 25 | // WriteTo writes the Map to the given writable 26 | func (m Map[K, V]) WriteTo(writable Writable) { 27 | writable.WriteUInt32LE(uint32(len(m))) 28 | 29 | for key, value := range m { 30 | m.writeType(key, writable) 31 | m.writeType(value, writable) 32 | } 33 | } 34 | 35 | func (m Map[K, V]) extractType(t any, readable Readable) error { 36 | // * This just makes Map.ExtractFrom() a bit cleaner 37 | // * since it doesn't have to type check 38 | if ptr, ok := t.(RVTypePtr); ok { 39 | return ptr.ExtractFrom(readable) 40 | } 41 | 42 | // * Maybe support other types..? 43 | 44 | return fmt.Errorf("Unsupported Map type %T", t) 45 | } 46 | 47 | // ExtractFrom extracts the Map from the given readable 48 | func (m *Map[K, V]) ExtractFrom(readable Readable) error { 49 | length, err := readable.ReadUInt32LE() 50 | if err != nil { 51 | return err 52 | } 53 | 54 | extracted := make(Map[K, V]) 55 | 56 | for i := 0; i < int(length); i++ { 57 | var key K 58 | if err := m.extractType(&key, readable); err != nil { 59 | return err 60 | } 61 | 62 | var value V 63 | if err := m.extractType(&value, readable); err != nil { 64 | return err 65 | } 66 | 67 | extracted[key] = value 68 | } 69 | 70 | *m = extracted 71 | 72 | return nil 73 | } 74 | 75 | func (m Map[K, V]) copyType(t any) RVType { 76 | // * This just makes Map.Copy() a bit cleaner 77 | // * since it doesn't have to type check 78 | if rvt, ok := t.(RVType); ok { 79 | return rvt.Copy() 80 | } 81 | 82 | // TODO - Improve this, this isn't safe 83 | return nil 84 | } 85 | 86 | // Copy returns a pointer to a copy of the Map. Requires type assertion when used 87 | func (m Map[K, V]) Copy() RVType { 88 | copied := make(Map[K, V]) 89 | 90 | for key, value := range m { 91 | copied[m.copyType(key).(K)] = value.Copy().(V) 92 | } 93 | 94 | return copied 95 | } 96 | 97 | func (m Map[K, V]) typesEqual(t1, t2 any) bool { 98 | // * This just makes Map.Equals() a bit cleaner 99 | // * since it doesn't have to type check 100 | if rvt1, ok := t1.(RVType); ok { 101 | if rvt2, ok := t2.(RVType); ok { 102 | return rvt1.Equals(rvt2) 103 | } 104 | } 105 | 106 | return false 107 | } 108 | 109 | // Equals checks if the input is equal in value to the current instance 110 | func (m Map[K, V]) Equals(o RVType) bool { 111 | if _, ok := o.(Map[K, V]); !ok { 112 | return false 113 | } 114 | 115 | other := o.(Map[K, V]) 116 | 117 | if len(m) != len(other) { 118 | return false 119 | } 120 | 121 | for key, value := range m { 122 | if otherValue, ok := other[key]; !ok || m.typesEqual(&value, &otherValue) { 123 | return false 124 | } 125 | } 126 | 127 | return true 128 | } 129 | 130 | // CopyRef copies the current value of the Map 131 | // and returns a pointer to the new copy 132 | func (m Map[K, V]) CopyRef() RVTypePtr { 133 | copied := m.Copy().(Map[K, V]) 134 | return &copied 135 | } 136 | 137 | // Deref takes a pointer to the Map 138 | // and dereferences it to the raw value. 139 | // Only useful when working with an instance of RVTypePtr 140 | func (m *Map[K, V]) Deref() RVType { 141 | return *m 142 | } 143 | 144 | // String returns a string representation of the struct 145 | func (m Map[K, V]) String() string { 146 | return m.FormatToString(0) 147 | } 148 | 149 | // FormatToString pretty-prints the struct data using the provided indentation level 150 | func (m Map[K, V]) FormatToString(indentationLevel int) string { 151 | indentationValues := strings.Repeat("\t", indentationLevel+1) 152 | indentationEnd := strings.Repeat("\t", indentationLevel) 153 | 154 | var b strings.Builder 155 | 156 | if len(m) == 0 { 157 | b.WriteString("{}\n") 158 | } else { 159 | b.WriteString("{\n") 160 | 161 | for key, value := range m { 162 | // TODO - Special handle the the last item to not add the comma on last item 163 | b.WriteString(fmt.Sprintf("%s%v: %v,\n", indentationValues, key, value)) 164 | } 165 | 166 | b.WriteString(fmt.Sprintf("%s}\n", indentationEnd)) 167 | } 168 | 169 | return b.String() 170 | } 171 | 172 | // NewMap returns a new Map of the provided type 173 | func NewMap[K comparable, V RVType]() Map[K, V] { 174 | return make(Map[K, V]) 175 | } 176 | -------------------------------------------------------------------------------- /byte_stream_in.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import ( 4 | "errors" 5 | 6 | crunch "github.com/superwhiskers/crunch/v3" 7 | ) 8 | 9 | // ByteStreamIn is an input stream abstraction of github.com/superwhiskers/crunch/v3 with nex type support 10 | type ByteStreamIn struct { 11 | *crunch.Buffer 12 | LibraryVersions *LibraryVersions 13 | Settings *ByteStreamSettings 14 | } 15 | 16 | // StringLengthSize returns the expected size of String length fields 17 | func (bsi *ByteStreamIn) StringLengthSize() int { 18 | size := 2 19 | 20 | if bsi.Settings != nil { 21 | size = bsi.Settings.StringLengthSize 22 | } 23 | 24 | return size 25 | } 26 | 27 | // PIDSize returns the size of PID types 28 | func (bsi *ByteStreamIn) PIDSize() int { 29 | size := 4 30 | 31 | if bsi.Settings != nil { 32 | size = bsi.Settings.PIDSize 33 | } 34 | 35 | return size 36 | } 37 | 38 | // UseStructureHeader determines if Structure headers should be used 39 | func (bsi *ByteStreamIn) UseStructureHeader() bool { 40 | useStructureHeader := false 41 | 42 | if bsi.Settings != nil { 43 | useStructureHeader = bsi.Settings.UseStructureHeader 44 | } 45 | 46 | return useStructureHeader 47 | } 48 | 49 | // Remaining returns the amount of data left to be read in the buffer 50 | func (bsi *ByteStreamIn) Remaining() uint64 { 51 | return uint64(len(bsi.Bytes()[bsi.ByteOffset():])) 52 | } 53 | 54 | // ReadRemaining reads all the data left to be read in the buffer 55 | func (bsi *ByteStreamIn) ReadRemaining() []byte { 56 | // * Can safely ignore this error, since bsi.Remaining() will never be less than itself 57 | remaining, _ := bsi.Read(uint64(bsi.Remaining())) 58 | 59 | return remaining 60 | } 61 | 62 | // Read reads the specified number of bytes. Returns an error if OOB 63 | func (bsi *ByteStreamIn) Read(length uint64) ([]byte, error) { 64 | if bsi.Remaining() < length { 65 | return []byte{}, errors.New("Read is OOB") 66 | } 67 | 68 | return bsi.ReadBytesNext(int64(length)), nil 69 | } 70 | 71 | // ReadUInt8 reads a uint8 72 | func (bsi *ByteStreamIn) ReadUInt8() (uint8, error) { 73 | if bsi.Remaining() < 1 { 74 | return 0, errors.New("Not enough data to read uint8") 75 | } 76 | 77 | return uint8(bsi.ReadByteNext()), nil 78 | } 79 | 80 | // ReadUInt16LE reads a Little-Endian encoded uint16 81 | func (bsi *ByteStreamIn) ReadUInt16LE() (uint16, error) { 82 | if bsi.Remaining() < 2 { 83 | return 0, errors.New("Not enough data to read uint16") 84 | } 85 | 86 | return bsi.ReadU16LENext(1)[0], nil 87 | } 88 | 89 | // ReadUInt32LE reads a Little-Endian encoded uint32 90 | func (bsi *ByteStreamIn) ReadUInt32LE() (uint32, error) { 91 | if bsi.Remaining() < 4 { 92 | return 0, errors.New("Not enough data to read uint32") 93 | } 94 | 95 | return bsi.ReadU32LENext(1)[0], nil 96 | } 97 | 98 | // ReadUInt64LE reads a Little-Endian encoded uint64 99 | func (bsi *ByteStreamIn) ReadUInt64LE() (uint64, error) { 100 | if bsi.Remaining() < 8 { 101 | return 0, errors.New("Not enough data to read uint64") 102 | } 103 | 104 | return bsi.ReadU64LENext(1)[0], nil 105 | } 106 | 107 | // ReadInt8 reads a uint8 108 | func (bsi *ByteStreamIn) ReadInt8() (int8, error) { 109 | if bsi.Remaining() < 1 { 110 | return 0, errors.New("Not enough data to read int8") 111 | } 112 | 113 | return int8(bsi.ReadByteNext()), nil 114 | } 115 | 116 | // ReadInt16LE reads a Little-Endian encoded int16 117 | func (bsi *ByteStreamIn) ReadInt16LE() (int16, error) { 118 | if bsi.Remaining() < 2 { 119 | return 0, errors.New("Not enough data to read int16") 120 | } 121 | 122 | return int16(bsi.ReadU16LENext(1)[0]), nil 123 | } 124 | 125 | // ReadInt32LE reads a Little-Endian encoded int32 126 | func (bsi *ByteStreamIn) ReadInt32LE() (int32, error) { 127 | if bsi.Remaining() < 4 { 128 | return 0, errors.New("Not enough data to read int32") 129 | } 130 | 131 | return int32(bsi.ReadU32LENext(1)[0]), nil 132 | } 133 | 134 | // ReadInt64LE reads a Little-Endian encoded int64 135 | func (bsi *ByteStreamIn) ReadInt64LE() (int64, error) { 136 | if bsi.Remaining() < 8 { 137 | return 0, errors.New("Not enough data to read int64") 138 | } 139 | 140 | return int64(bsi.ReadU64LENext(1)[0]), nil 141 | } 142 | 143 | // ReadFloat32LE reads a Little-Endian encoded float32 144 | func (bsi *ByteStreamIn) ReadFloat32LE() (float32, error) { 145 | if bsi.Remaining() < 4 { 146 | return 0, errors.New("Not enough data to read float32") 147 | } 148 | 149 | return bsi.ReadF32LENext(1)[0], nil 150 | } 151 | 152 | // ReadFloat64LE reads a Little-Endian encoded float64 153 | func (bsi *ByteStreamIn) ReadFloat64LE() (float64, error) { 154 | if bsi.Remaining() < 8 { 155 | return 0, errors.New("Not enough data to read float64") 156 | } 157 | 158 | return bsi.ReadF64LENext(1)[0], nil 159 | } 160 | 161 | // ReadBool reads a bool 162 | func (bsi *ByteStreamIn) ReadBool() (bool, error) { 163 | if bsi.Remaining() < 1 { 164 | return false, errors.New("Not enough data to read bool") 165 | } 166 | 167 | return bsi.ReadByteNext() == 1, nil 168 | } 169 | 170 | // NewByteStreamIn returns a new NEX input byte stream 171 | func NewByteStreamIn(data []byte, libraryVersions *LibraryVersions, settings *ByteStreamSettings) *ByteStreamIn { 172 | return &ByteStreamIn{ 173 | Buffer: crunch.NewBuffer(data), 174 | LibraryVersions: libraryVersions, 175 | Settings: settings, 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /types/rv_connection_data.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // RVConnectionData is an implementation of rdv::RVConnectionData. 9 | // Contains the locations and data of Rendez-Vous connection. 10 | type RVConnectionData struct { 11 | Structure 12 | StationURL StationURL `json:"station_url" db:"station_url" bson:"station_url" xml:"StationURL"` 13 | SpecialProtocols List[UInt8] `json:"special_protocols" db:"special_protocols" bson:"special_protocols" xml:"SpecialProtocols"` 14 | StationURLSpecialProtocols StationURL `json:"station_url_special_protocols" db:"station_url_special_protocols" bson:"station_url_special_protocols" xml:"StationURLSpecialProtocols"` 15 | Time DateTime `json:"time" db:"time" bson:"time" xml:"Time"` 16 | } 17 | 18 | // WriteTo writes the RVConnectionData to the given writable 19 | func (rvcd RVConnectionData) WriteTo(writable Writable) { 20 | contentWritable := writable.CopyNew() 21 | 22 | rvcd.StationURL.WriteTo(contentWritable) 23 | rvcd.SpecialProtocols.WriteTo(contentWritable) 24 | rvcd.StationURLSpecialProtocols.WriteTo(contentWritable) 25 | 26 | if rvcd.StructureVersion >= 1 { 27 | rvcd.Time.WriteTo(contentWritable) 28 | } 29 | 30 | content := contentWritable.Bytes() 31 | 32 | rvcd.WriteHeaderTo(writable, uint32(len(content))) 33 | 34 | writable.Write(content) 35 | } 36 | 37 | // ExtractFrom extracts the RVConnectionData from the given readable 38 | func (rvcd *RVConnectionData) ExtractFrom(readable Readable) error { 39 | var err error 40 | if err = rvcd.ExtractHeaderFrom(readable); err != nil { 41 | return fmt.Errorf("Failed to read RVConnectionData header. %s", err.Error()) 42 | } 43 | 44 | err = rvcd.StationURL.ExtractFrom(readable) 45 | if err != nil { 46 | return fmt.Errorf("Failed to read RVConnectionData.StationURL. %s", err.Error()) 47 | } 48 | 49 | err = rvcd.SpecialProtocols.ExtractFrom(readable) 50 | if err != nil { 51 | return fmt.Errorf("Failed to read RVConnectionData.SpecialProtocols. %s", err.Error()) 52 | } 53 | 54 | err = rvcd.StationURLSpecialProtocols.ExtractFrom(readable) 55 | if err != nil { 56 | return fmt.Errorf("Failed to read RVConnectionData.StationURLSpecialProtocols. %s", err.Error()) 57 | } 58 | 59 | if rvcd.StructureVersion >= 1 { 60 | err := rvcd.Time.ExtractFrom(readable) 61 | if err != nil { 62 | return fmt.Errorf("Failed to read RVConnectionData.Time. %s", err.Error()) 63 | } 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // Copy returns a new copied instance of RVConnectionData 70 | func (rvcd RVConnectionData) Copy() RVType { 71 | copied := NewRVConnectionData() 72 | 73 | copied.StructureVersion = rvcd.StructureVersion 74 | copied.StationURL = rvcd.StationURL.Copy().(StationURL) 75 | copied.SpecialProtocols = rvcd.SpecialProtocols.Copy().(List[UInt8]) 76 | copied.StationURLSpecialProtocols = rvcd.StationURLSpecialProtocols.Copy().(StationURL) 77 | 78 | if rvcd.StructureVersion >= 1 { 79 | copied.Time = *rvcd.Time.Copy().(*DateTime) 80 | } 81 | 82 | return copied 83 | } 84 | 85 | // Equals checks if the input is equal in value to the current instance 86 | func (rvcd RVConnectionData) Equals(o RVType) bool { 87 | if _, ok := o.(RVConnectionData); !ok { 88 | return false 89 | } 90 | 91 | other := o.(RVConnectionData) 92 | 93 | if rvcd.StructureVersion != other.StructureVersion { 94 | return false 95 | } 96 | 97 | if !rvcd.StationURL.Equals(other.StationURL) { 98 | return false 99 | } 100 | 101 | if !rvcd.SpecialProtocols.Equals(other.SpecialProtocols) { 102 | return false 103 | } 104 | 105 | if !rvcd.StationURLSpecialProtocols.Equals(other.StationURLSpecialProtocols) { 106 | return false 107 | } 108 | 109 | if rvcd.StructureVersion >= 1 { 110 | return rvcd.Time.Equals(other.Time) 111 | } 112 | 113 | return true 114 | } 115 | 116 | // CopyRef copies the current value of the RVConnectionData 117 | // and returns a pointer to the new copy 118 | func (rvcd RVConnectionData) CopyRef() RVTypePtr { 119 | copied := rvcd.Copy().(RVConnectionData) 120 | return &copied 121 | } 122 | 123 | // Deref takes a pointer to the RVConnectionData 124 | // and dereferences it to the raw value. 125 | // Only useful when working with an instance of RVTypePtr 126 | func (rvcd *RVConnectionData) Deref() RVType { 127 | return *rvcd 128 | } 129 | 130 | // String returns a string representation of the struct 131 | func (rvcd RVConnectionData) String() string { 132 | return rvcd.FormatToString(0) 133 | } 134 | 135 | // FormatToString pretty-prints the struct data using the provided indentation level 136 | func (rvcd RVConnectionData) FormatToString(indentationLevel int) string { 137 | indentationValues := strings.Repeat("\t", indentationLevel+1) 138 | indentationEnd := strings.Repeat("\t", indentationLevel) 139 | 140 | var b strings.Builder 141 | 142 | b.WriteString("RVConnectionData{\n") 143 | b.WriteString(fmt.Sprintf("%sStructureVersion: %d,\n", indentationValues, rvcd.StructureVersion)) 144 | b.WriteString(fmt.Sprintf("%sStationURL: %s,\n", indentationValues, rvcd.StationURL.FormatToString(indentationLevel+1))) 145 | b.WriteString(fmt.Sprintf("%sSpecialProtocols: %s,\n", indentationValues, rvcd.SpecialProtocols)) 146 | b.WriteString(fmt.Sprintf("%sStationURLSpecialProtocols: %s,\n", indentationValues, rvcd.StationURLSpecialProtocols.FormatToString(indentationLevel+1))) 147 | b.WriteString(fmt.Sprintf("%sTime: %s\n", indentationValues, rvcd.Time.FormatToString(indentationLevel+1))) 148 | b.WriteString(fmt.Sprintf("%s}", indentationEnd)) 149 | 150 | return b.String() 151 | } 152 | 153 | // NewRVConnectionData returns a new RVConnectionData 154 | func NewRVConnectionData() RVConnectionData { 155 | rvcd := RVConnectionData{ 156 | StationURL: NewStationURL(""), 157 | SpecialProtocols: NewList[UInt8](), 158 | StationURLSpecialProtocols: NewStationURL(""), 159 | Time: NewDateTime(0), 160 | } 161 | 162 | return rvcd 163 | } 164 | -------------------------------------------------------------------------------- /test/auth.go: -------------------------------------------------------------------------------- 1 | // Package main implements a test server 2 | package main 3 | 4 | import ( 5 | "encoding/hex" 6 | "fmt" 7 | 8 | "github.com/PretendoNetwork/nex-go/v2" 9 | "github.com/PretendoNetwork/nex-go/v2/constants" 10 | "github.com/PretendoNetwork/nex-go/v2/types" 11 | ) 12 | 13 | var authServer *nex.PRUDPServer 14 | var authEndpoint *nex.PRUDPEndPoint 15 | 16 | func startAuthenticationServer() { 17 | fmt.Println("Starting auth") 18 | 19 | authServer = nex.NewPRUDPServer() 20 | 21 | authEndpoint = nex.NewPRUDPEndPoint(1) 22 | 23 | authServer.EnableMetrics("0.0.0.0:9090") 24 | 25 | authEndpoint.AccountDetailsByPID = accountDetailsByPID 26 | authEndpoint.AccountDetailsByUsername = accountDetailsByUsername 27 | authEndpoint.ServerAccount = authenticationServerAccount 28 | 29 | authEndpoint.OnData(func(packet nex.PacketInterface) { 30 | if packet, ok := packet.(nex.PRUDPPacketInterface); ok { 31 | request := packet.RMCMessage() 32 | 33 | fmt.Println("[AUTH]", request.ProtocolID, request.MethodID) 34 | 35 | if request.ProtocolID == 0xA { // * Ticket Granting 36 | if request.MethodID == 0x1 { 37 | login(packet) 38 | } 39 | 40 | if request.MethodID == 0x3 { 41 | requestTicket(packet) 42 | } 43 | } 44 | } 45 | }) 46 | 47 | authServer.SetFragmentSize(962) 48 | authServer.LibraryVersions.SetDefault(nex.NewLibraryVersion(1, 1, 0)) 49 | authServer.SessionKeyLength = 16 50 | authServer.AccessKey = "ridfebb9" 51 | authServer.BindPRUDPEndPoint(authEndpoint) 52 | authServer.Listen(60000) 53 | } 54 | 55 | func login(packet nex.PRUDPPacketInterface) { 56 | request := packet.RMCMessage() 57 | response := nex.NewRMCMessage(authEndpoint) 58 | 59 | parameters := request.Parameters 60 | 61 | parametersStream := nex.NewByteStreamIn(parameters, authEndpoint.LibraryVersions(), authEndpoint.ByteStreamSettings()) 62 | 63 | strUserName := types.NewString("") 64 | if err := strUserName.ExtractFrom(parametersStream); err != nil { 65 | panic(err) 66 | } 67 | 68 | sourceAccount, _ := accountDetailsByUsername(string(strUserName)) 69 | targetAccount, _ := accountDetailsByUsername(secureServerAccount.Username) 70 | 71 | retval := types.NewQResultSuccess(0x00010001) 72 | pidPrincipal := sourceAccount.PID 73 | pbufResponse := types.NewBuffer(generateTicket(sourceAccount, targetAccount, authServer.SessionKeyLength)) 74 | pConnectionData := types.NewRVConnectionData() 75 | strReturnMsg := types.NewString("Test Build") 76 | 77 | pConnectionData.StationURL = types.NewStationURL("prudps:/address=192.168.1.98;port=60001;CID=1;PID=2;sid=1;stream=10;type=2") 78 | pConnectionData.SpecialProtocols = types.NewList[types.UInt8]() 79 | pConnectionData.StationURLSpecialProtocols = types.NewStationURL("") 80 | pConnectionData.Time = types.NewDateTime(0).Now() 81 | 82 | responseStream := nex.NewByteStreamOut(authEndpoint.LibraryVersions(), authEndpoint.ByteStreamSettings()) 83 | 84 | retval.WriteTo(responseStream) 85 | pidPrincipal.WriteTo(responseStream) 86 | pbufResponse.WriteTo(responseStream) 87 | pConnectionData.WriteTo(responseStream) 88 | strReturnMsg.WriteTo(responseStream) 89 | 90 | response.IsSuccess = true 91 | response.IsRequest = false 92 | response.ErrorCode = 0x00010001 93 | response.ProtocolID = request.ProtocolID 94 | response.CallID = request.CallID 95 | response.MethodID = request.MethodID 96 | response.Parameters = responseStream.Bytes() 97 | 98 | responsePacket, _ := nex.NewPRUDPPacketV0(authServer, packet.Sender().(*nex.PRUDPConnection), nil) 99 | 100 | responsePacket.SetType(packet.Type()) 101 | responsePacket.AddFlag(constants.PacketFlagHasSize) 102 | responsePacket.AddFlag(constants.PacketFlagReliable) 103 | responsePacket.AddFlag(constants.PacketFlagNeedsAck) 104 | responsePacket.SetSourceVirtualPortStreamType(packet.DestinationVirtualPortStreamType()) 105 | responsePacket.SetSourceVirtualPortStreamID(packet.DestinationVirtualPortStreamID()) 106 | responsePacket.SetDestinationVirtualPortStreamType(packet.SourceVirtualPortStreamType()) 107 | responsePacket.SetDestinationVirtualPortStreamID(packet.SourceVirtualPortStreamID()) 108 | responsePacket.SetSubstreamID(packet.SubstreamID()) 109 | responsePacket.SetPayload(response.Bytes()) 110 | 111 | fmt.Println(hex.EncodeToString(responsePacket.Payload())) 112 | 113 | authServer.Send(responsePacket) 114 | } 115 | 116 | func requestTicket(packet nex.PRUDPPacketInterface) { 117 | request := packet.RMCMessage() 118 | response := nex.NewRMCMessage(authEndpoint) 119 | 120 | parameters := request.Parameters 121 | 122 | parametersStream := nex.NewByteStreamIn(parameters, authEndpoint.LibraryVersions(), authEndpoint.ByteStreamSettings()) 123 | 124 | idSource := types.NewPID(0) 125 | if err := idSource.ExtractFrom(parametersStream); err != nil { 126 | panic(err) 127 | } 128 | 129 | idTarget := types.NewPID(0) 130 | if err := idTarget.ExtractFrom(parametersStream); err != nil { 131 | panic(err) 132 | } 133 | 134 | sourceAccount, _ := accountDetailsByPID(idSource) 135 | targetAccount, _ := accountDetailsByPID(idTarget) 136 | 137 | retval := types.NewQResultSuccess(0x00010001) 138 | pbufResponse := types.NewBuffer(generateTicket(sourceAccount, targetAccount, authServer.SessionKeyLength)) 139 | 140 | responseStream := nex.NewByteStreamOut(authEndpoint.LibraryVersions(), authEndpoint.ByteStreamSettings()) 141 | 142 | retval.WriteTo(responseStream) 143 | pbufResponse.WriteTo(responseStream) 144 | 145 | response.IsSuccess = true 146 | response.IsRequest = false 147 | response.ErrorCode = 0x00010001 148 | response.ProtocolID = request.ProtocolID 149 | response.CallID = request.CallID 150 | response.MethodID = request.MethodID 151 | response.Parameters = responseStream.Bytes() 152 | 153 | responsePacket, _ := nex.NewPRUDPPacketV0(authServer, packet.Sender().(*nex.PRUDPConnection), nil) 154 | 155 | responsePacket.SetType(packet.Type()) 156 | responsePacket.AddFlag(constants.PacketFlagHasSize) 157 | responsePacket.AddFlag(constants.PacketFlagReliable) 158 | responsePacket.AddFlag(constants.PacketFlagNeedsAck) 159 | responsePacket.SetSourceVirtualPortStreamType(packet.DestinationVirtualPortStreamType()) 160 | responsePacket.SetSourceVirtualPortStreamID(packet.DestinationVirtualPortStreamID()) 161 | responsePacket.SetDestinationVirtualPortStreamType(packet.SourceVirtualPortStreamType()) 162 | responsePacket.SetDestinationVirtualPortStreamID(packet.SourceVirtualPortStreamID()) 163 | responsePacket.SetSubstreamID(packet.SubstreamID()) 164 | responsePacket.SetPayload(response.Bytes()) 165 | 166 | fmt.Println(hex.EncodeToString(responsePacket.Payload())) 167 | 168 | authServer.Send(responsePacket) 169 | } 170 | -------------------------------------------------------------------------------- /kerberos.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/md5" 6 | "crypto/rand" 7 | "crypto/rc4" 8 | "errors" 9 | "fmt" 10 | 11 | "github.com/PretendoNetwork/nex-go/v2/types" 12 | ) 13 | 14 | // KerberosEncryption is a struct representing a Kerberos encryption utility 15 | type KerberosEncryption struct { 16 | key []byte 17 | } 18 | 19 | // Validate checks the integrity of the given buffer by verifying the HMAC checksum 20 | func (ke *KerberosEncryption) Validate(buffer []byte) bool { 21 | data := buffer[:len(buffer)-0x10] 22 | checksum := buffer[len(buffer)-0x10:] 23 | mac := hmac.New(md5.New, ke.key) 24 | 25 | mac.Write(data) 26 | 27 | return hmac.Equal(checksum, mac.Sum(nil)) 28 | } 29 | 30 | // Decrypt decrypts the provided buffer if it passes the integrity check 31 | func (ke *KerberosEncryption) Decrypt(buffer []byte) ([]byte, error) { 32 | if !ke.Validate(buffer) { 33 | return nil, errors.New("Invalid Kerberos checksum (incorrect password)") 34 | } 35 | 36 | cipher, err := rc4.NewCipher(ke.key) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | decrypted := make([]byte, len(buffer)-0x10) 42 | 43 | cipher.XORKeyStream(decrypted, buffer[:len(buffer)-0x10]) 44 | 45 | return decrypted, nil 46 | } 47 | 48 | // Encrypt encrypts the given buffer and appends an HMAC checksum for integrity 49 | func (ke *KerberosEncryption) Encrypt(buffer []byte) []byte { 50 | cipher, _ := rc4.NewCipher(ke.key) 51 | encrypted := make([]byte, len(buffer)) 52 | 53 | cipher.XORKeyStream(encrypted, buffer) 54 | 55 | mac := hmac.New(md5.New, ke.key) 56 | 57 | mac.Write(encrypted) 58 | 59 | checksum := mac.Sum(nil) 60 | 61 | return append(encrypted, checksum...) 62 | } 63 | 64 | // NewKerberosEncryption creates a new KerberosEncryption instance with the given key. 65 | func NewKerberosEncryption(key []byte) *KerberosEncryption { 66 | return &KerberosEncryption{key: key} 67 | } 68 | 69 | // KerberosTicket represents a ticket granting a user access to a secure server 70 | type KerberosTicket struct { 71 | SessionKey []byte 72 | TargetPID types.PID 73 | InternalData types.Buffer 74 | } 75 | 76 | // Encrypt writes the ticket data to the provided stream and returns the encrypted byte slice 77 | func (kt *KerberosTicket) Encrypt(key []byte, stream *ByteStreamOut) ([]byte, error) { 78 | encryption := NewKerberosEncryption(key) 79 | 80 | stream.Grow(int64(len(kt.SessionKey))) 81 | stream.WriteBytesNext(kt.SessionKey) 82 | 83 | kt.TargetPID.WriteTo(stream) 84 | kt.InternalData.WriteTo(stream) 85 | 86 | return encryption.Encrypt(stream.Bytes()), nil 87 | } 88 | 89 | // NewKerberosTicket returns a new Ticket instance 90 | func NewKerberosTicket() *KerberosTicket { 91 | return &KerberosTicket{} 92 | } 93 | 94 | // KerberosTicketInternalData holds the internal data for a kerberos ticket to be processed by the server 95 | type KerberosTicketInternalData struct { 96 | Server *PRUDPServer // TODO - Remove this dependency and make a settings struct 97 | Issued types.DateTime 98 | SourcePID types.PID 99 | SessionKey []byte 100 | } 101 | 102 | // Encrypt writes the ticket data to the provided stream and returns the encrypted byte slice 103 | func (ti *KerberosTicketInternalData) Encrypt(key []byte, stream *ByteStreamOut) ([]byte, error) { 104 | ti.Issued.WriteTo(stream) 105 | ti.SourcePID.WriteTo(stream) 106 | 107 | stream.Grow(int64(len(ti.SessionKey))) 108 | stream.WriteBytesNext(ti.SessionKey) 109 | 110 | data := stream.Bytes() 111 | 112 | if ti.Server.KerberosTicketVersion == 1 { 113 | ticketKey := make([]byte, 16) 114 | _, err := rand.Read(ticketKey) 115 | if err != nil { 116 | return nil, fmt.Errorf("Failed to generate ticket key. %s", err.Error()) 117 | } 118 | 119 | hash := md5.Sum(append(key, ticketKey...)) 120 | finalKey := hash[:] 121 | 122 | encryption := NewKerberosEncryption(finalKey) 123 | 124 | encrypted := encryption.Encrypt(data) 125 | 126 | finalStream := NewByteStreamOut(stream.LibraryVersions, stream.Settings) 127 | 128 | ticketBuffer := types.NewBuffer(ticketKey) 129 | encryptedBuffer := types.NewBuffer(encrypted) 130 | 131 | ticketBuffer.WriteTo(finalStream) 132 | encryptedBuffer.WriteTo(finalStream) 133 | 134 | return finalStream.Bytes(), nil 135 | } 136 | 137 | encryption := NewKerberosEncryption([]byte(key)) 138 | 139 | return encryption.Encrypt(data), nil 140 | } 141 | 142 | // Decrypt decrypts the given data and populates the struct 143 | func (ti *KerberosTicketInternalData) Decrypt(stream *ByteStreamIn, key []byte) error { 144 | if ti.Server.KerberosTicketVersion == 1 { 145 | ticketKey := types.NewBuffer(nil) 146 | if err := ticketKey.ExtractFrom(stream); err != nil { 147 | return fmt.Errorf("Failed to read Kerberos ticket internal data key. %s", err.Error()) 148 | } 149 | 150 | data := types.NewBuffer(nil) 151 | if err := data.ExtractFrom(stream); err != nil { 152 | return fmt.Errorf("Failed to read Kerberos ticket internal data. %s", err.Error()) 153 | } 154 | 155 | hash := md5.Sum(append(key, ticketKey...)) 156 | key = hash[:] 157 | 158 | stream = NewByteStreamIn(data, stream.LibraryVersions, stream.Settings) 159 | } 160 | 161 | encryption := NewKerberosEncryption(key) 162 | 163 | decrypted, err := encryption.Decrypt(stream.Bytes()) 164 | if err != nil { 165 | return fmt.Errorf("Failed to decrypt Kerberos ticket internal data. %s", err.Error()) 166 | } 167 | 168 | stream = NewByteStreamIn(decrypted, stream.LibraryVersions, stream.Settings) 169 | 170 | timestamp := types.NewDateTime(0) 171 | if err := timestamp.ExtractFrom(stream); err != nil { 172 | return fmt.Errorf("Failed to read Kerberos ticket internal data timestamp %s", err.Error()) 173 | } 174 | 175 | userPID := types.NewPID(0) 176 | if err := userPID.ExtractFrom(stream); err != nil { 177 | return fmt.Errorf("Failed to read Kerberos ticket internal data user PID %s", err.Error()) 178 | } 179 | 180 | ti.Issued = timestamp 181 | ti.SourcePID = userPID 182 | ti.SessionKey = stream.ReadBytesNext(int64(ti.Server.SessionKeyLength)) 183 | 184 | return nil 185 | } 186 | 187 | // NewKerberosTicketInternalData returns a new KerberosTicketInternalData instance 188 | func NewKerberosTicketInternalData(server *PRUDPServer) *KerberosTicketInternalData { 189 | return &KerberosTicketInternalData{Server: server} 190 | } 191 | 192 | // DeriveKerberosKey derives a users kerberos encryption key based on their PID and password 193 | func DeriveKerberosKey(pid types.PID, password []byte) []byte { 194 | iterationCount := int(65000 + pid%1024) 195 | key := password 196 | hash := make([]byte, md5.Size) 197 | 198 | for i := 0; i < iterationCount; i++ { 199 | sum := md5.Sum(key) 200 | copy(hash, sum[:]) 201 | key = hash 202 | } 203 | 204 | return key 205 | } 206 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/PretendoNetwork/plogger-go v1.0.4 h1:PF7xHw9eDRHH+RsAP9tmAE7fG0N0p6H4iPwHKnsoXwc= 2 | github.com/PretendoNetwork/plogger-go v1.0.4/go.mod h1:7kD6M4vPq1JL4LTuPg6kuB1OvUBOwQOtAvTaUwMbwvU= 3 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 6 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= 11 | github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= 12 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 13 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 14 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 15 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 16 | github.com/jwalton/go-supportscolor v1.2.0 h1:g6Ha4u7Vm3LIsQ5wmeBpS4gazu0UP1DRDE8y6bre4H8= 17 | github.com/jwalton/go-supportscolor v1.2.0/go.mod h1:hFVUAZV2cWg+WFFC4v8pT2X/S2qUUBYMioBD9AINXGs= 18 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 19 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 20 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 21 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 22 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 23 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 24 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 25 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 26 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 27 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 28 | github.com/lxzan/gws v1.8.8 h1:st193ZG8qN8sSw8/g/UituFhs7etmKzS7jUqhijg5wM= 29 | github.com/lxzan/gws v1.8.8/go.mod h1:FcGeRMB7HwGuTvMLR24ku0Zx0p6RXqeKASeMc4VYgi4= 30 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 31 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 32 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 33 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 34 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 35 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 36 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 37 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 38 | github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= 39 | github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 40 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 41 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 42 | github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= 43 | github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= 44 | github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 45 | github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 46 | github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e h1:dCWirM5F3wMY+cmRda/B1BiPsFtmzXqV9b0hLWtVBMs= 47 | github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e/go.mod h1:9leZcVcItj6m9/CfHY5Em/iBrCz7js8LcRQGTKEEv2M= 48 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 49 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 50 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 51 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 52 | github.com/superwhiskers/crunch/v3 v3.5.7 h1:N9RLxaR65C36i26BUIpzPXGy2f6pQ7wisu2bawbKNqg= 53 | github.com/superwhiskers/crunch/v3 v3.5.7/go.mod h1:4ub2EKgF1MAhTjoOCTU4b9uLMsAweHEa89aRrfAypXA= 54 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 55 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 56 | go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= 57 | go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= 58 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= 59 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= 60 | golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= 61 | golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 62 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 63 | golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 64 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= 66 | golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 67 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 68 | golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= 69 | golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= 70 | google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= 71 | google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 72 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 73 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 74 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 75 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 76 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 77 | -------------------------------------------------------------------------------- /hpp_server.go: -------------------------------------------------------------------------------- 1 | package nex 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "strconv" 9 | 10 | "github.com/PretendoNetwork/nex-go/v2/types" 11 | ) 12 | 13 | // HPPServer represents a bare-bones HPP server 14 | type HPPServer struct { 15 | server *http.Server 16 | accessKey string 17 | libraryVersions *LibraryVersions 18 | dataHandlers []func(packet PacketInterface) 19 | errorEventHandlers []func(err *Error) 20 | byteStreamSettings *ByteStreamSettings 21 | AccountDetailsByPID func(pid types.PID) (*Account, *Error) 22 | AccountDetailsByUsername func(username string) (*Account, *Error) 23 | useVerboseRMC bool 24 | } 25 | 26 | // RegisterServiceProtocol registers a NEX service with the HPP server 27 | func (s *HPPServer) RegisterServiceProtocol(protocol ServiceProtocol) { 28 | protocol.SetEndpoint(s) 29 | s.OnData(protocol.HandlePacket) 30 | } 31 | 32 | // OnData adds an event handler which is fired when a new HPP request is received 33 | func (s *HPPServer) OnData(handler func(packet PacketInterface)) { 34 | s.dataHandlers = append(s.dataHandlers, handler) 35 | } 36 | 37 | // EmitError calls all the endpoints error event handlers with the provided error 38 | func (s *HPPServer) EmitError(err *Error) { 39 | for _, handler := range s.errorEventHandlers { 40 | go handler(err) 41 | } 42 | } 43 | 44 | func (s *HPPServer) handleRequest(w http.ResponseWriter, req *http.Request) { 45 | if req.Method != "POST" { 46 | w.WriteHeader(http.StatusBadRequest) 47 | return 48 | } 49 | 50 | pidValue := req.Header.Get("pid") 51 | if pidValue == "" { 52 | w.WriteHeader(http.StatusBadRequest) 53 | return 54 | } 55 | 56 | // * The server checks that the header exists, but doesn't verify the value 57 | token := req.Header.Get("token") 58 | if token == "" { 59 | w.WriteHeader(http.StatusBadRequest) 60 | return 61 | } 62 | 63 | accessKeySignature := req.Header.Get("signature1") 64 | if accessKeySignature == "" { 65 | w.WriteHeader(http.StatusBadRequest) 66 | return 67 | } 68 | 69 | passwordSignature := req.Header.Get("signature2") 70 | if passwordSignature == "" { 71 | w.WriteHeader(http.StatusBadRequest) 72 | return 73 | } 74 | 75 | pid, err := strconv.Atoi(pidValue) 76 | if err != nil { 77 | w.WriteHeader(http.StatusBadRequest) 78 | return 79 | } 80 | 81 | rmcRequestString := req.FormValue("file") 82 | 83 | rmcRequestBytes := []byte(rmcRequestString) 84 | 85 | tcpAddr, err := net.ResolveTCPAddr("tcp", req.RemoteAddr) 86 | if err != nil { 87 | // * Should never happen? 88 | logger.Error(err.Error()) 89 | w.WriteHeader(http.StatusBadRequest) 90 | return 91 | } 92 | 93 | client := NewHPPClient(tcpAddr, s) 94 | client.SetPID(types.NewPID(uint64(pid))) 95 | 96 | hppPacket, err := NewHPPPacket(client, rmcRequestBytes) 97 | if err != nil { 98 | logger.Error(err.Error()) 99 | w.WriteHeader(http.StatusBadRequest) 100 | return 101 | } 102 | 103 | err = hppPacket.validateAccessKeySignature(accessKeySignature) 104 | if err != nil { 105 | logger.Error(err.Error()) 106 | w.WriteHeader(http.StatusBadRequest) 107 | return 108 | } 109 | 110 | err = hppPacket.validatePasswordSignature(passwordSignature) 111 | if err != nil { 112 | logger.Error(err.Error()) 113 | 114 | rmcMessage := hppPacket.RMCMessage() 115 | 116 | // HPP returns PythonCore::ValidationError if password is missing or invalid 117 | errorResponse := NewRMCError(s, ResultCodes.PythonCore.ValidationError) 118 | errorResponse.CallID = rmcMessage.CallID 119 | errorResponse.IsHPP = true 120 | 121 | _, err = w.Write(errorResponse.Bytes()) 122 | if err != nil { 123 | logger.Error(err.Error()) 124 | } 125 | 126 | return 127 | } 128 | 129 | for _, dataHandler := range s.dataHandlers { 130 | go dataHandler(hppPacket) 131 | } 132 | 133 | <-hppPacket.processed 134 | 135 | if len(hppPacket.payload) > 0 { 136 | _, err = w.Write(hppPacket.payload) 137 | if err != nil { 138 | logger.Error(err.Error()) 139 | } 140 | } 141 | } 142 | 143 | // Listen starts a HPP server on a given port 144 | func (s *HPPServer) Listen(port int) { 145 | s.server.Addr = fmt.Sprintf(":%d", port) 146 | 147 | err := s.server.ListenAndServe() 148 | if err != nil { 149 | panic(err) 150 | } 151 | } 152 | 153 | // ListenSecure starts a HPP server on a given port using a secure (TLS) server 154 | func (s *HPPServer) ListenSecure(port int, certFile, keyFile string) { 155 | s.server.Addr = fmt.Sprintf(":%d", port) 156 | 157 | err := s.server.ListenAndServeTLS(certFile, keyFile) 158 | if err != nil { 159 | panic(err) 160 | } 161 | } 162 | 163 | // Send sends the packet to the packets sender 164 | func (s *HPPServer) Send(packet PacketInterface) { 165 | if packet, ok := packet.(*HPPPacket); ok { 166 | packet.message.IsHPP = true 167 | packet.payload = packet.message.Bytes() 168 | 169 | packet.processed <- true 170 | } 171 | } 172 | 173 | // LibraryVersions returns the versions that the server has 174 | func (s *HPPServer) LibraryVersions() *LibraryVersions { 175 | return s.libraryVersions 176 | } 177 | 178 | // AccessKey returns the servers sandbox access key 179 | func (s *HPPServer) AccessKey() string { 180 | return s.accessKey 181 | } 182 | 183 | // SetAccessKey sets the servers sandbox access key 184 | func (s *HPPServer) SetAccessKey(accessKey string) { 185 | s.accessKey = accessKey 186 | } 187 | 188 | // ByteStreamSettings returns the settings to be used for ByteStreams 189 | func (s *HPPServer) ByteStreamSettings() *ByteStreamSettings { 190 | return s.byteStreamSettings 191 | } 192 | 193 | // SetByteStreamSettings sets the settings to be used for ByteStreams 194 | func (s *HPPServer) SetByteStreamSettings(byteStreamSettings *ByteStreamSettings) { 195 | s.byteStreamSettings = byteStreamSettings 196 | } 197 | 198 | // UseVerboseRMC checks whether or not the endpoint uses verbose RMC 199 | func (s *HPPServer) UseVerboseRMC() bool { 200 | return s.useVerboseRMC 201 | } 202 | 203 | // EnableVerboseRMC enable or disables the use of verbose RMC 204 | func (s *HPPServer) EnableVerboseRMC(enable bool) { 205 | s.useVerboseRMC = enable 206 | } 207 | 208 | // NewHPPServer returns a new HPP server 209 | func NewHPPServer() *HPPServer { 210 | s := &HPPServer{ 211 | dataHandlers: make([]func(packet PacketInterface), 0), 212 | errorEventHandlers: make([]func(err *Error), 0), 213 | libraryVersions: NewLibraryVersions(), 214 | byteStreamSettings: NewByteStreamSettings(), 215 | } 216 | 217 | mux := http.NewServeMux() 218 | mux.HandleFunc("/hpp/", s.handleRequest) 219 | 220 | httpServer := &http.Server{ 221 | Handler: mux, 222 | TLSConfig: &tls.Config{ 223 | MinVersion: tls.VersionTLS11, // * The 3DS and Wii U only support up to TLS 1.1 natively 224 | }, 225 | } 226 | 227 | s.server = httpServer 228 | 229 | return s 230 | } 231 | -------------------------------------------------------------------------------- /types/quuid.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "bytes" 5 | "database/sql/driver" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "slices" 10 | "strings" 11 | ) 12 | 13 | // QUUID is an implementation of rdv::qUUID. 14 | // Type alias of []byte. 15 | // Encodes a UUID in little-endian byte order. 16 | type QUUID []byte 17 | 18 | // WriteTo writes the QUUID to the given writable 19 | func (qu QUUID) WriteTo(writable Writable) { 20 | writable.Write(qu) 21 | } 22 | 23 | // ExtractFrom extracts the QUUID from the given readable 24 | func (qu *QUUID) ExtractFrom(readable Readable) error { 25 | if readable.Remaining() < 16 { 26 | return errors.New("Not enough data left to read qUUID") 27 | } 28 | 29 | *qu, _ = readable.Read(16) 30 | return nil 31 | } 32 | 33 | // Copy returns a new copied instance of qUUID 34 | func (qu QUUID) Copy() RVType { 35 | return NewQUUID(qu) 36 | } 37 | 38 | // Equals checks if the passed Structure contains the same data as the current instance 39 | func (qu QUUID) Equals(o RVType) bool { 40 | if _, ok := o.(QUUID); !ok { 41 | return false 42 | } 43 | 44 | return bytes.Equal(qu, o.(QUUID)) 45 | } 46 | 47 | // CopyRef copies the current value of the QUUID 48 | // and returns a pointer to the new copy 49 | func (qu QUUID) CopyRef() RVTypePtr { 50 | copied := qu.Copy().(QUUID) 51 | return &copied 52 | } 53 | 54 | // Deref takes a pointer to the QUUID 55 | // and dereferences it to the raw value. 56 | // Only useful when working with an instance of RVTypePtr 57 | func (qu *QUUID) Deref() RVType { 58 | return *qu 59 | } 60 | 61 | // String returns a string representation of the struct 62 | func (qu QUUID) String() string { 63 | return qu.FormatToString(0) 64 | } 65 | 66 | // FormatToString pretty-prints the struct data using the provided indentation level 67 | func (qu QUUID) FormatToString(indentationLevel int) string { 68 | indentationValues := strings.Repeat("\t", indentationLevel+1) 69 | indentationEnd := strings.Repeat("\t", indentationLevel) 70 | 71 | var b strings.Builder 72 | 73 | b.WriteString("qUUID{\n") 74 | b.WriteString(fmt.Sprintf("%sUUID: %s\n", indentationValues, qu.GetStringValue())) 75 | b.WriteString(fmt.Sprintf("%s}", indentationEnd)) 76 | 77 | return b.String() 78 | } 79 | 80 | // GetStringValue returns the UUID encoded in the qUUID 81 | func (qu QUUID) GetStringValue() string { 82 | // * Create copy of the data since slices.Reverse modifies the slice in-line 83 | data := make([]byte, len(qu)) 84 | copy(data, qu) 85 | 86 | if len(data) != 16 { 87 | // * Default dummy UUID as found in WATCH_DOGS 88 | return "00000000-0000-0000-0000-000000000002" 89 | } 90 | 91 | section1 := data[0:4] 92 | section2 := data[4:6] 93 | section3 := data[6:8] 94 | section4 := data[8:10] 95 | section5_1 := data[10:12] 96 | section5_2 := data[12:14] 97 | section5_3 := data[14:16] 98 | 99 | slices.Reverse(section1) 100 | slices.Reverse(section2) 101 | slices.Reverse(section3) 102 | slices.Reverse(section4) 103 | slices.Reverse(section5_1) 104 | slices.Reverse(section5_2) 105 | slices.Reverse(section5_3) 106 | 107 | var b strings.Builder 108 | 109 | b.WriteString(hex.EncodeToString(section1)) 110 | b.WriteString("-") 111 | b.WriteString(hex.EncodeToString(section2)) 112 | b.WriteString("-") 113 | b.WriteString(hex.EncodeToString(section3)) 114 | b.WriteString("-") 115 | b.WriteString(hex.EncodeToString(section4)) 116 | b.WriteString("-") 117 | b.WriteString(hex.EncodeToString(section5_1)) 118 | b.WriteString(hex.EncodeToString(section5_2)) 119 | b.WriteString(hex.EncodeToString(section5_3)) 120 | 121 | return b.String() 122 | } 123 | 124 | // FromString converts a UUID string to a qUUID 125 | func (qu *QUUID) FromString(uuid string) error { 126 | sections := strings.Split(uuid, "-") 127 | if len(sections) != 5 { 128 | return fmt.Errorf("Invalid UUID. Not enough sections. Expected 5, got %d", len(sections)) 129 | } 130 | 131 | data := make([]byte, 0, 16) 132 | 133 | var appendSection = func(section string, expectedSize int) error { 134 | sectionBytes, err := hex.DecodeString(section) 135 | if err != nil { 136 | return err 137 | } 138 | 139 | if len(sectionBytes) != expectedSize { 140 | return fmt.Errorf("Unexpected section size. Expected %d, got %d", expectedSize, len(sectionBytes)) 141 | } 142 | 143 | data = append(data, sectionBytes...) 144 | 145 | return nil 146 | } 147 | 148 | if err := appendSection(sections[0], 4); err != nil { 149 | return fmt.Errorf("Failed to read UUID section 1. %s", err.Error()) 150 | } 151 | 152 | if err := appendSection(sections[1], 2); err != nil { 153 | return fmt.Errorf("Failed to read UUID section 2. %s", err.Error()) 154 | } 155 | 156 | if err := appendSection(sections[2], 2); err != nil { 157 | return fmt.Errorf("Failed to read UUID section 3. %s", err.Error()) 158 | } 159 | 160 | if err := appendSection(sections[3], 2); err != nil { 161 | return fmt.Errorf("Failed to read UUID section 4. %s", err.Error()) 162 | } 163 | 164 | if err := appendSection(sections[4], 6); err != nil { 165 | return fmt.Errorf("Failed to read UUID section 5. %s", err.Error()) 166 | } 167 | 168 | slices.Reverse(data[0:4]) 169 | slices.Reverse(data[4:6]) 170 | slices.Reverse(data[6:8]) 171 | slices.Reverse(data[8:10]) 172 | slices.Reverse(data[10:12]) 173 | slices.Reverse(data[12:14]) 174 | slices.Reverse(data[14:16]) 175 | 176 | *qu = data 177 | 178 | return nil 179 | } 180 | 181 | // Value implements the sql.Valuer interface for QUUID. 182 | // Returns the result of QUUID.GetStringValue() 183 | // 184 | // Only designed for Postgres databases, and only for 185 | // `text` and `uuid` column types 186 | func (qu QUUID) Value() (driver.Value, error) { 187 | if len(qu) == 0 { 188 | return nil, nil 189 | } 190 | 191 | if len(qu) != 16 { 192 | return nil, fmt.Errorf("invalid QUUID bytes length: %d", len(qu)) 193 | } 194 | 195 | // * Just return as a basic UUID, not one of the 196 | // * fancy ones Postgres supports 197 | return qu.GetStringValue(), nil 198 | } 199 | 200 | // Scan implements the sql.Scanner interface for QUUID 201 | // 202 | // Only designed for Postgres databases 203 | func (qu *QUUID) Scan(value any) error { 204 | if value == nil { 205 | return nil 206 | } 207 | 208 | var uuid string 209 | switch v := value.(type) { 210 | case string: 211 | uuid = v 212 | case []byte: 213 | uuid = string(v) 214 | default: 215 | return fmt.Errorf("cannot scan type %T into QUUID", v) 216 | } 217 | 218 | // * While Postgres supports many formats for UUID *inputs*, 219 | // * the UUID *output* is always in the standard form: 220 | // * https://www.postgresql.org/docs/current/datatype-uuid.html 221 | // * "Output is always in the standard form." 222 | 223 | return qu.FromString(uuid) 224 | } 225 | 226 | // NewQUUID returns a new qUUID 227 | func NewQUUID(input []byte) QUUID { 228 | qu := make(QUUID, len(input)) 229 | copy(qu, input) 230 | 231 | return qu 232 | } 233 | -------------------------------------------------------------------------------- /types/list.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql" 5 | "database/sql/driver" 6 | "encoding/csv" 7 | "encoding/hex" 8 | "fmt" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // List is an implementation of rdv::qList. 14 | // This data type holds an array of other types. 15 | // 16 | // Unlike Buffer and qBuffer, which use the same data type with differing size field lengths, 17 | // there does not seem to be an official rdv::List type 18 | type List[T RVType] []T 19 | 20 | // WriteTo writes the List to the given writable 21 | func (l List[T]) WriteTo(writable Writable) { 22 | writable.WriteUInt32LE(uint32(len(l))) 23 | 24 | for _, v := range l { 25 | v.WriteTo(writable) 26 | } 27 | } 28 | 29 | func (l List[T]) extractType(t any, readable Readable) error { 30 | // * This just makes List.ExtractFrom() a bit cleaner 31 | // * since it doesn't have to type check 32 | if ptr, ok := t.(RVTypePtr); ok { 33 | return ptr.ExtractFrom(readable) 34 | } 35 | 36 | // * Maybe support other types..? 37 | 38 | return fmt.Errorf("Unsupported List type %T", t) 39 | } 40 | 41 | // ExtractFrom extracts the List from the given readable 42 | func (l *List[T]) ExtractFrom(readable Readable) error { 43 | length, err := readable.ReadUInt32LE() 44 | if err != nil { 45 | return err 46 | } 47 | 48 | slice := make([]T, 0, length) 49 | 50 | for i := 0; i < int(length); i++ { 51 | var value T 52 | if err := l.extractType(&value, readable); err != nil { 53 | return err 54 | } 55 | 56 | slice = append(slice, value) 57 | } 58 | 59 | *l = slice 60 | 61 | return nil 62 | } 63 | 64 | // Copy returns a pointer to a copy of the List. Requires type assertion when used 65 | func (l List[T]) Copy() RVType { 66 | copied := make(List[T], 0) 67 | 68 | for _, v := range l { 69 | copied = append(copied, v.Copy().(T)) 70 | } 71 | 72 | return copied 73 | } 74 | 75 | // Equals checks if the input is equal in value to the current instance 76 | func (l List[T]) Equals(o RVType) bool { 77 | if _, ok := o.(List[T]); !ok { 78 | return false 79 | } 80 | 81 | other := o.(List[T]) 82 | 83 | if len(l) != len(other) { 84 | return false 85 | } 86 | 87 | for i := 0; i < len(l); i++ { 88 | if !l[i].Equals(other[i]) { 89 | return false 90 | } 91 | } 92 | 93 | return true 94 | } 95 | 96 | // CopyRef copies the current value of the List 97 | // and returns a pointer to the new copy 98 | func (l List[T]) CopyRef() RVTypePtr { 99 | copied := l.Copy().(List[T]) 100 | return &copied 101 | } 102 | 103 | // Deref takes a pointer to the List 104 | // and dereferences it to the raw value. 105 | // Only useful when working with an instance of RVTypePtr 106 | func (l *List[T]) Deref() RVType { 107 | return *l 108 | } 109 | 110 | // Contains checks if the provided value exists in the List 111 | func (l List[T]) Contains(checkValue T) bool { 112 | for _, v := range l { 113 | if v.Equals(checkValue) { 114 | return true 115 | } 116 | } 117 | 118 | return false 119 | } 120 | 121 | // String returns a string representation of the struct 122 | func (l List[T]) String() string { 123 | return fmt.Sprintf("%v", ([]T)(l)) 124 | } 125 | 126 | // Scan implements sql.Scanner for database/sql 127 | func (l *List[T]) Scan(value any) error { 128 | if value == nil { 129 | *l = List[T]{} 130 | return nil 131 | } 132 | 133 | var pgArray string 134 | switch v := value.(type) { 135 | case []byte: 136 | pgArray = string(v) 137 | case string: 138 | pgArray = v 139 | default: 140 | return fmt.Errorf("unsupported Scan type for List: %T", value) 141 | } 142 | 143 | if len(pgArray) < 2 || pgArray[0] != '{' || pgArray[len(pgArray)-1] != '}' { 144 | return fmt.Errorf("invalid PostgreSQL array format: %s", pgArray) 145 | } 146 | 147 | pgArray = pgArray[1 : len(pgArray)-1] 148 | 149 | if pgArray == "" { 150 | *l = List[T]{} 151 | return nil 152 | } 153 | 154 | var elements []string 155 | 156 | if strings.HasPrefix(pgArray, "\"") { 157 | reader := csv.NewReader(strings.NewReader(pgArray)) 158 | reader.Comma = ',' 159 | 160 | var err error 161 | elements, err = reader.Read() 162 | 163 | if err != nil { 164 | return fmt.Errorf("failed to parse array elements: %w", err) 165 | } 166 | } else { 167 | elements = strings.Split(pgArray, ",") 168 | } 169 | 170 | var zero T 171 | isByteaType := false 172 | 173 | switch any(zero).(type) { 174 | case Buffer, QBuffer: 175 | isByteaType = true 176 | } 177 | 178 | result := make(List[T], 0, len(elements)) 179 | 180 | for _, element := range elements { 181 | var new T 182 | ptr := any(&new) 183 | 184 | if scanner, ok := ptr.(sql.Scanner); ok { 185 | var scanValue any = element 186 | 187 | if isByteaType { 188 | hexStr := element 189 | hexStr = strings.TrimPrefix(hexStr, "\\\\x") 190 | hexStr = strings.TrimPrefix(hexStr, "\\x") 191 | bytes, err := hex.DecodeString(hexStr) 192 | 193 | if err != nil { 194 | return fmt.Errorf("failed to decode hex for bytea element %q: %w", element, err) 195 | } 196 | 197 | scanValue = bytes 198 | } 199 | 200 | if err := scanner.Scan(scanValue); err != nil { 201 | return fmt.Errorf("failed to scan element: %w", err) 202 | } 203 | 204 | result = append(result, new) 205 | } else { 206 | return fmt.Errorf("list element type %T does not implement sql.Scanner", zero) 207 | } 208 | } 209 | 210 | *l = result 211 | 212 | return nil 213 | } 214 | 215 | // Value implements driver.Valuer for database/sql 216 | func (l List[T]) Value() (driver.Value, error) { 217 | if len(l) == 0 { 218 | return "{}", nil 219 | } 220 | 221 | var b strings.Builder 222 | b.WriteString("{") 223 | 224 | for i, new := range l { 225 | if i > 0 { 226 | b.WriteString(",") 227 | } 228 | 229 | if valuer, ok := any(new).(driver.Valuer); ok { 230 | v, err := valuer.Value() 231 | if err != nil { 232 | return nil, fmt.Errorf("failed to get value for element %d: %w", i, err) 233 | } 234 | 235 | switch val := v.(type) { 236 | case string: 237 | escaped := strings.ReplaceAll(val, "\\", "\\\\") 238 | escaped = strings.ReplaceAll(escaped, "\"", "\\\"") 239 | 240 | b.WriteString("\"") 241 | b.WriteString(escaped) 242 | b.WriteString("\"") 243 | case int64, float64, bool: 244 | b.WriteString(fmt.Sprintf("%v", val)) 245 | case []byte: 246 | b.WriteString("\"\\\\x") 247 | b.WriteString(hex.EncodeToString(val)) 248 | b.WriteString("\"") 249 | case time.Time: 250 | b.WriteString("\"") 251 | b.WriteString(val.Format("2006-01-02 15:04:05")) 252 | b.WriteString("\"") 253 | case nil: 254 | b.WriteString("NULL") 255 | default: 256 | return nil, fmt.Errorf("unsupported value type %T for element %d", val, i) 257 | } 258 | } else { 259 | return nil, fmt.Errorf("element type %T does not implement driver.Valuer", new) 260 | } 261 | } 262 | 263 | b.WriteString("}") 264 | 265 | return b.String(), nil 266 | } 267 | 268 | // NewList returns a new List of the provided type 269 | func NewList[T RVType]() List[T] { 270 | return make(List[T], 0) 271 | } 272 | -------------------------------------------------------------------------------- /types/datetime.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // DateTime is an implementation of rdv::DateTime. 12 | // Type alias of uint64. 13 | // The underlying value is a uint64 bit field containing date and time information. 14 | type DateTime uint64 15 | 16 | // WriteTo writes the DateTime to the given writable 17 | func (dt DateTime) WriteTo(writable Writable) { 18 | writable.WriteUInt64LE(uint64(dt)) 19 | } 20 | 21 | // ExtractFrom extracts the DateTime from the given readable 22 | func (dt *DateTime) ExtractFrom(readable Readable) error { 23 | value, err := readable.ReadUInt64LE() 24 | if err != nil { 25 | return fmt.Errorf("Failed to read DateTime value. %s", err.Error()) 26 | } 27 | 28 | *dt = DateTime(value) 29 | return nil 30 | } 31 | 32 | // Copy returns a new copied instance of DateTime 33 | func (dt DateTime) Copy() RVType { 34 | return NewDateTime(uint64(dt)) 35 | } 36 | 37 | // Equals checks if the input is equal in value to the current instance 38 | func (dt DateTime) Equals(o RVType) bool { 39 | if _, ok := o.(DateTime); !ok { 40 | return false 41 | } 42 | 43 | return dt == o.(DateTime) 44 | } 45 | 46 | // CopyRef copies the current value of the DateTime 47 | // and returns a pointer to the new copy 48 | func (dt DateTime) CopyRef() RVTypePtr { 49 | copied := dt.Copy().(DateTime) 50 | return &copied 51 | } 52 | 53 | // Deref takes a pointer to the DateTime 54 | // and dereferences it to the raw value. 55 | // Only useful when working with an instance of RVTypePtr 56 | func (dt *DateTime) Deref() RVType { 57 | return *dt 58 | } 59 | 60 | // Make initilizes a DateTime with the input data 61 | func (dt *DateTime) Make(year, month, day, hour, minute, second int) DateTime { 62 | *dt = DateTime(second | (minute << 6) | (hour << 12) | (day << 17) | (month << 22) | (year << 26)) 63 | 64 | return *dt 65 | } 66 | 67 | // FromTimestamp converts a Time timestamp into a NEX DateTime 68 | func (dt *DateTime) FromTimestamp(timestamp time.Time) DateTime { 69 | year := timestamp.Year() 70 | month := int(timestamp.Month()) 71 | day := timestamp.Day() 72 | hour := timestamp.Hour() 73 | minute := timestamp.Minute() 74 | second := timestamp.Second() 75 | 76 | return dt.Make(year, month, day, hour, minute, second) 77 | } 78 | 79 | // Now returns a NEX DateTime value of the current UTC time 80 | func (dt DateTime) Now() DateTime { 81 | return dt.FromTimestamp(time.Now().UTC()) 82 | } 83 | 84 | // Second returns the seconds value stored in the DateTime 85 | func (dt DateTime) Second() int { 86 | return int(dt & 63) 87 | } 88 | 89 | // Minute returns the minutes value stored in the DateTime 90 | func (dt DateTime) Minute() int { 91 | return int((dt >> 6) & 63) 92 | } 93 | 94 | // Hour returns the hours value stored in the DateTime 95 | func (dt DateTime) Hour() int { 96 | return int((dt >> 12) & 31) 97 | } 98 | 99 | // Day returns the day value stored in the DateTime 100 | func (dt DateTime) Day() int { 101 | return int((dt >> 17) & 31) 102 | } 103 | 104 | // Month returns the month value stored in the DateTime 105 | func (dt DateTime) Month() time.Month { 106 | return time.Month((dt >> 22) & 15) 107 | } 108 | 109 | // Year returns the year value stored in the DateTime 110 | func (dt DateTime) Year() int { 111 | return int(dt >> 26) 112 | } 113 | 114 | // Standard returns the DateTime as a standard time.Time 115 | func (dt DateTime) Standard() time.Time { 116 | return time.Date( 117 | dt.Year(), 118 | dt.Month(), 119 | dt.Day(), 120 | dt.Hour(), 121 | dt.Minute(), 122 | dt.Second(), 123 | 0, 124 | time.UTC, 125 | ) 126 | } 127 | 128 | // String returns a string representation of the struct 129 | func (dt DateTime) String() string { 130 | return dt.FormatToString(0) 131 | } 132 | 133 | // FormatToString pretty-prints the struct data using the provided indentation level 134 | func (dt DateTime) FormatToString(indentationLevel int) string { 135 | indentationValues := strings.Repeat("\t", indentationLevel+1) 136 | indentationEnd := strings.Repeat("\t", indentationLevel) 137 | 138 | var b strings.Builder 139 | 140 | b.WriteString("DateTime{\n") 141 | b.WriteString(fmt.Sprintf("%svalue: %d (%s)\n", indentationValues, dt, dt.Standard().Format("2006-01-02 15:04:05"))) 142 | b.WriteString(fmt.Sprintf("%s}", indentationEnd)) 143 | 144 | return b.String() 145 | } 146 | 147 | // Value implements the sql.Valuer interface for DateTime. 148 | // Returns the result of DateTime.Standard() 149 | // 150 | // Only designed for Postgres databases, and only for 151 | // `timestamp` column types WITHOUT a timezone. 152 | // `timestamp WITH TIME ZONE` is NOT supported. 153 | // 154 | // Value is always returned in UTC 155 | func (dt DateTime) Value() (driver.Value, error) { 156 | // TODO - Treating 0-value DateTimes as SQL NULL. Is this correct? 157 | if dt == 0 { 158 | return nil, nil 159 | } 160 | 161 | // * Treat DateTime as a time.Time for SQL purposes 162 | return dt.Standard(), nil 163 | } 164 | 165 | // Scan implements the sql.Scanner interface for DateTime 166 | // 167 | // Only designed for Postgres databases, and only for 168 | // `timestamp` column types WITHOUT a timezone. 169 | // `timestamp WITH TIME ZONE` is NOT supported. 170 | // 171 | // Assumes the value is already in UTC 172 | func (dt *DateTime) Scan(value any) error { 173 | // TODO - Treating SQL NULL as a 0-value DateTime. Is this correct? 174 | if value == nil { 175 | *dt = 0 176 | return nil 177 | } 178 | 179 | // * We shouldn't actually see anything besides 180 | // * time.Time here, but handle all possible 181 | // * cases just the be safe 182 | switch v := value.(type) { 183 | case int64: 184 | *dt = DateTime(uint64(v)) 185 | return nil 186 | case uint64: 187 | *dt = DateTime(v) 188 | return nil 189 | case []byte: 190 | return dt.scanSQLString(string(v)) 191 | case string: 192 | return dt.scanSQLString(v) 193 | case time.Time: 194 | dt.FromTimestamp(v) 195 | return nil 196 | default: 197 | return fmt.Errorf("DateTime.Scan: cannot convert %T to DateTime", value) 198 | } 199 | } 200 | 201 | // scanSQLString suppliments DateTime.Scan() to help 202 | // parse the various string formats the `timestamp` 203 | // could be in 204 | func (dt *DateTime) scanSQLString(str string) error { 205 | // * First attempt, try date strings. 206 | // * Attempt the constant first before 207 | // * heuristically trying others 208 | timeFormats := []string{ 209 | time.RFC3339, 210 | "2006-01-02 15:04:05", 211 | "2006-01-02T15:04:05", 212 | "2006-01-02 15:04:05Z07:00", 213 | } 214 | 215 | for _, format := range timeFormats { 216 | if t, err := time.Parse(format, str); err == nil { 217 | dt.FromTimestamp(t) 218 | return nil 219 | } 220 | } 221 | 222 | // * Second attempt, assume it's a uint64 223 | // * stored as a string, and assume the uint64 224 | // * is a DateTime uint64, NOT a UNIX timestamp 225 | // TODO - Can we determine if this is a UNIX timestamp vs DateTime uint64? Heuristics? 226 | if val, err := strconv.ParseUint(str, 10, 64); err == nil { 227 | *dt = DateTime(val) 228 | return nil 229 | } 230 | 231 | // * Something is super wrong 232 | return fmt.Errorf("DateTime.Scan: cannot convert %q to DateTime", str) 233 | } 234 | 235 | // NewDateTime returns a new DateTime instance 236 | func NewDateTime(input uint64) DateTime { 237 | dt := DateTime(input) 238 | return dt 239 | } 240 | --------------------------------------------------------------------------------