aboutsummaryrefslogtreecommitdiff
path: root/hotline/tracker.go
blob: b927986ae65f27d922baf61139143f1e2b69a658 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
package hotline

import (
	"bufio"
	"encoding/binary"
	"fmt"
	"io"
	"net"
	"slices"
	"strconv"
	"time"
)

// TrackerRegistration represents the payload a Hotline server sends to a Tracker to register
type TrackerRegistration struct {
	Port        [2]byte // Server's listening TCP port number
	UserCount   int     // Number of users connected to this particular server
	PassID      [4]byte // Random number generated by the server
	Name        string  // Server Name
	Description string  // Description of the server

	readOffset int // Internal offset to track read progress
}

// Read implements io.Reader to write tracker registration payload bytes to slice
func (tr *TrackerRegistration) Read(p []byte) (int, error) {
	userCount := make([]byte, 2)
	binary.BigEndian.PutUint16(userCount, uint16(tr.UserCount))

	buf := slices.Concat(
		[]byte{0x00, 0x01}, // Magic number, always 1
		tr.Port[:],
		userCount,
		[]byte{0x00, 0x00}, // Magic number, always 0
		tr.PassID[:],
		[]byte{uint8(len(tr.Name))},
		[]byte(tr.Name),
		[]byte{uint8(len(tr.Description))},
		[]byte(tr.Description),
	)

	if tr.readOffset >= len(buf) {
		return 0, io.EOF // All bytes have been read
	}

	n := copy(p, buf[tr.readOffset:])
	tr.readOffset += n

	return n, nil
}

func register(tracker string, tr *TrackerRegistration) error {
	conn, err := net.Dial("udp", tracker)
	if err != nil {
		return fmt.Errorf("failed to dial tracker: %w", err)
	}
	defer conn.Close()

	if _, err := io.Copy(conn, tr); err != nil {
		return fmt.Errorf("failed to write to connection: %w", err)
	}

	return nil
}

const trackerTimeout = 5 * time.Second

// All string values use 8-bit ASCII character set encoding.
// Client Interface with Tracker
// After establishing a connection with tracker, the following information is sent:
// Description	Size	Data	Note
// Magic number	4	‘HTRK’
// 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
type ServerInfoHeader struct {
	MsgType     [2]byte // always has value of 1
	MsgDataSize [2]byte // Remaining size of request
	SrvCount    [2]byte // Number of servers in the server list
	SrvCountDup [2]byte // Same as previous field ¯\_(ツ)_/¯
}

type ServerRecord struct {
	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 Name
	DescriptionSize byte
	Description     []byte
}

func GetListing(addr string) ([]ServerRecord, error) {
	conn, err := net.DialTimeout("tcp", addr, trackerTimeout)
	if err != nil {
		return []ServerRecord{}, err
	}
	defer func() { _ = conn.Close() }()

	_, err = conn.Write(
		[]byte{
			0x48, 0x54, 0x52, 0x4B, // HTRK
			0x00, 0x01, // Version
		},
	)
	if err != nil {
		return nil, err
	}

	var th TrackerHeader
	if err := binary.Read(conn, binary.BigEndian, &th); err != nil {
		return nil, err
	}

	var info ServerInfoHeader
	if err := binary.Read(conn, binary.BigEndian, &info); err != nil {
		return nil, err
	}

	totalSrv := int(binary.BigEndian.Uint16(info.SrvCount[:]))

	scanner := bufio.NewScanner(conn)
	scanner.Split(serverScanner)

	var servers []ServerRecord
	for {
		scanner.Scan()

		var srv ServerRecord
		_, err = srv.Write(scanner.Bytes())
		if err != nil {
			return nil, err
		}

		servers = append(servers, srv)
		if len(servers) == totalSrv {
			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) {
	// The name length field is the 11th byte of the server record.  If we don't have that many bytes,
	// return nil token so the Scanner reads more data and continues scanning.
	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])

	// The description length field is at the 12th + nameLen byte of the server record.
	// If we don't have that many bytes, return nil token so the Scanner reads more data and continues scanning.
	if len(data) < 11+nameLen {
		return 0, nil, nil
	}

	// Next we need the description length from the 11+nameLen byte:
	descLen := int(data[11+nameLen])

	if len(data) < 12+nameLen+descLen {
		return 0, nil, nil
	}

	return 12 + nameLen + descLen, data[0 : 12+nameLen+descLen], nil
}

// Write implements io.Writer for ServerRecord
func (s *ServerRecord) Write(b []byte) (n int, err error) {
	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) Addr() string {
	return fmt.Sprintf("%s:%s",
		net.IP(s.IPAddr[:]),
		strconv.Itoa(int(binary.BigEndian.Uint16(s.Port[:]))),
	)
}