package hotline
import (
- "bytes"
+ "bufio"
"encoding/binary"
"fmt"
"github.com/jhalter/mobius/concat"
"time"
)
+// TrackerRegistration represents the payload a Hotline server sends to a Tracker to register
type TrackerRegistration struct {
- Port []byte // Server’s listening UDP port number TODO: wat?
- UserCount int // Number of users connected to this particular server
- PassID []byte // Random number generated by the server
- Name string // Server’s name
- Description string // Description of the server
+ Port [2]byte // Server listening port number
+ UserCount int // Number of users connected to this particular server
+ PassID []byte // Random number generated by the server
+ Name string // Server name
+ Description string // Description of the server
}
-func (tr *TrackerRegistration) Payload() []byte {
+// TODO: reimplement as io.Reader
+func (tr *TrackerRegistration) Read() []byte {
userCount := make([]byte, 2)
binary.BigEndian.PutUint16(userCount, uint16(tr.UserCount))
return concat.Slices(
[]byte{0x00, 0x01},
- tr.Port,
+ tr.Port[:],
userCount,
[]byte{0x00, 0x00},
tr.PassID,
)
}
-func register(tracker string, tr TrackerRegistration) error {
+func register(tracker string, tr *TrackerRegistration) error {
conn, err := net.Dial("udp", tracker)
if err != nil {
return err
}
- if _, err := conn.Write(tr.Payload()); err != nil {
+ b := tr.Read()
+
+ if _, err := conn.Write(b); err != nil {
return err
}
return nil
}
-type ServerListing struct {
-}
-
const trackerTimeout = 5 * time.Second
// All string values use 8-bit ASCII character set encoding.
// Version 2 1 or 2 Old protocol (1) or new (2)
// Reply received from the tracker starts with a header:
-//
-
type TrackerHeader struct {
Protocol [4]byte // "HTRK" 0x4854524B
Version [2]byte // Old protocol (1) or new (2)
}
-//Message type 2 1 Sending list of servers
-//Message data size 2 Remaining size of this request
-//Number of servers 2 Number of servers in the server list
-//Number of servers 2 Same as previous field
+// Message type 2 1 Sending list of servers
+// Message data size 2 Remaining size of this request
+// Number of servers 2 Number of servers in the server list
+// Number of servers 2 Same as previous field
type ServerInfoHeader struct {
MsgType [2]byte // always has value of 1
MsgDataSize [2]byte // Remaining size of request
}
type ServerRecord struct {
- IPAddr []byte
- Port []byte
- NumUsers []byte // Number of users connected to this particular server
- Unused []byte
+ IPAddr [4]byte
+ Port [2]byte
+ NumUsers [2]byte // Number of users connected to this particular server
+ Unused [2]byte
NameSize byte // Length of name string
- Name []byte // Server’s name
+ Name []byte // Server name
DescriptionSize byte
Description []byte
}
func GetListing(addr string) ([]ServerRecord, error) {
conn, err := net.DialTimeout("tcp", addr, trackerTimeout)
if err != nil {
- return nil, err
+ return []ServerRecord{}, err
}
- //spew.Dump(conn)
+ defer func() { _ = conn.Close() }()
+
_, err = conn.Write(
[]byte{
0x48, 0x54, 0x52, 0x4B, // HTRK
return nil, err
}
- totalRead := 0
-
- buf := make([]byte, 4096) // handshakes are always 12 bytes in length
- var readLen int
- if readLen, err = conn.Read(buf); err != nil {
- return nil, err
- }
- totalRead += readLen
-
var th TrackerHeader
- if err := binary.Read(bytes.NewReader(buf[:6]), binary.BigEndian, &th); err != nil {
+ if err := binary.Read(conn, binary.BigEndian, &th); err != nil {
return nil, err
}
var info ServerInfoHeader
- if err := binary.Read(bytes.NewReader(buf[6:14]), binary.BigEndian, &info); err != nil {
+ if err := binary.Read(conn, binary.BigEndian, &info); err != nil {
return nil, err
}
- payloadSize := int(binary.BigEndian.Uint16(info.MsgDataSize[:]))
-
- if totalRead < payloadSize {
- for {
- //fmt.Printf("totalRead: %v", totalRead)
- //fmt.Printf("readLen: %v Payload size: %v, Server count: %x\n", readLen, payloadSize, info.SrvCount)
-
- if readLen, err = conn.Read(buf); err != nil {
- return nil, err
- }
- totalRead += readLen
- if totalRead >= payloadSize {
- break
- }
- }
- }
- //fmt.Println("passed read")
totalSrv := int(binary.BigEndian.Uint16(info.SrvCount[:]))
- //fmt.Printf("readLen: %v Payload size: %v, Server count: %x\n", readLen, payloadSize, info.SrvCount)
- srvBuf := buf[14:totalRead]
-
- totalRead += readLen
+ scanner := bufio.NewScanner(conn)
+ scanner.Split(serverScanner)
var servers []ServerRecord
for {
+ scanner.Scan()
var srv ServerRecord
- n, _ := srv.Read(srvBuf)
- servers = append(servers, srv)
+ _, _ = srv.Read(scanner.Bytes())
- srvBuf = srvBuf[n:]
- // fmt.Printf("srvBuf len: %v\n", len(srvBuf))
+ servers = append(servers, srv)
if len(servers) == totalSrv {
- return servers, nil
- }
-
- if len(srvBuf) == 0 {
- if readLen, err = conn.Read(buf); err != nil {
- return nil, err
- }
- srvBuf = buf[8:readLen]
+ break
}
}
return servers, nil
}
+// serverScanner implements bufio.SplitFunc for parsing the tracker list into ServerRecords tokens
+// Example payload:
+// 00000000 18 05 30 63 15 7c 00 02 00 00 10 54 68 65 20 4d |..0c.|.....The M|
+// 00000010 6f 62 69 75 73 20 53 74 72 69 70 40 48 6f 6d 65 |obius Strip@Home|
+// 00000020 20 6f 66 20 74 68 65 20 4d 6f 62 69 75 73 20 48 | of the Mobius H|
+// 00000030 6f 74 6c 69 6e 65 20 73 65 72 76 65 72 20 61 6e |otline server an|
+// 00000040 64 20 63 6c 69 65 6e 74 20 7c 20 54 52 54 50 48 |d client | TRTPH|
+// 00000050 4f 54 4c 2e 63 6f 6d 3a 35 35 30 30 2d 4f 3a b2 |OTL.com:5500-O:.|
+// 00000060 15 7c 00 00 00 00 08 53 65 6e 65 63 74 75 73 20 |.|.....Senectus |
+func serverScanner(data []byte, _ bool) (advance int, token []byte, err error) {
+ if len(data) < 10 {
+ return 0, nil, nil
+ }
+
+ // A server entry has two variable length fields: the name and description.
+ // To get the token length, we first need the name length from the 10th byte:
+ nameLen := int(data[10])
+
+ // Next we need the description length from the 11+nameLen byte:
+ descLen := int(data[11+nameLen])
+
+ return 12 + nameLen + descLen, data[0 : 12+nameLen+descLen], nil
+}
+
+// Read implements io.Reader for ServerRecord
func (s *ServerRecord) Read(b []byte) (n int, err error) {
- s.IPAddr = b[0:4]
- s.Port = b[4:6]
- s.NumUsers = b[6:8]
- s.NameSize = b[10]
+ copy(s.IPAddr[:], b[0:4])
+ copy(s.Port[:], b[4:6])
+ copy(s.NumUsers[:], b[6:8])
nameLen := int(b[10])
+
s.Name = b[11 : 11+nameLen]
s.DescriptionSize = b[11+nameLen]
s.Description = b[12+nameLen : 12+nameLen+int(s.DescriptionSize)]
return 12 + nameLen + int(s.DescriptionSize), nil
}
-func (s *ServerRecord) PortInt() int {
- data := binary.BigEndian.Uint16(s.Port)
- return int(data)
-}
-
func (s *ServerRecord) Addr() string {
return fmt.Sprintf("%s:%s",
- net.IP(s.IPAddr),
- strconv.Itoa(int(binary.BigEndian.Uint16(s.Port))),
+ net.IP(s.IPAddr[:]),
+ strconv.Itoa(int(binary.BigEndian.Uint16(s.Port[:]))),
)
}