Skip to content

Commit 4cded9e

Browse files
committed
gamed: answer state checker probes
1 parent 3eeade3 commit 4cded9e

6 files changed

Lines changed: 252 additions & 22 deletions

File tree

internal/boot/flow.go

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
gameflow "github.com/MikelCalvo/go-metin2-server/internal/game"
77
"github.com/MikelCalvo/go-metin2-server/internal/handshake"
88
loginflow "github.com/MikelCalvo/go-metin2-server/internal/login"
9+
"github.com/MikelCalvo/go-metin2-server/internal/proto/control"
910
"github.com/MikelCalvo/go-metin2-server/internal/proto/frame"
1011
"github.com/MikelCalvo/go-metin2-server/internal/session"
1112
worldentry "github.com/MikelCalvo/go-metin2-server/internal/worldentry"
@@ -17,19 +18,25 @@ var (
1718
)
1819

1920
type Config struct {
20-
Handshake handshake.Config
21-
Login loginflow.Config
22-
WorldEntry worldentry.Config
23-
Game gameflow.Config
21+
Handshake handshake.Config
22+
Login loginflow.Config
23+
StateChecker StateCheckerConfig
24+
WorldEntry worldentry.Config
25+
Game gameflow.Config
26+
}
27+
28+
type StateCheckerConfig struct {
29+
Channels []control.ChannelStatus
2430
}
2531

2632
type Flow struct {
27-
machine *session.StateMachine
28-
handshake *handshake.Flow
29-
login *loginflow.Flow
30-
worldEntry *worldentry.Flow
31-
game *gameflow.Flow
32-
configErr error
33+
machine *session.StateMachine
34+
handshake *handshake.Flow
35+
login *loginflow.Flow
36+
worldEntry *worldentry.Flow
37+
game *gameflow.Flow
38+
stateChecker StateCheckerConfig
39+
configErr error
3340
}
3441

3542
func NewFlow(cfg Config) *Flow {
@@ -43,12 +50,13 @@ func NewFlow(cfg Config) *Flow {
4350
handshakeCfg.NextPhase = session.PhaseLogin
4451

4552
return &Flow{
46-
machine: machine,
47-
handshake: handshake.NewFlow(machine, handshakeCfg),
48-
login: loginflow.NewFlow(machine, cfg.Login),
49-
worldEntry: worldentry.NewFlow(machine, cfg.WorldEntry),
50-
game: gameflow.NewFlow(machine, cfg.Game),
51-
configErr: configErr,
53+
machine: machine,
54+
handshake: handshake.NewFlow(machine, handshakeCfg),
55+
login: loginflow.NewFlow(machine, cfg.Login),
56+
worldEntry: worldentry.NewFlow(machine, cfg.WorldEntry),
57+
game: gameflow.NewFlow(machine, cfg.Game),
58+
stateChecker: cfg.StateChecker,
59+
configErr: configErr,
5260
}
5361
}
5462

@@ -67,6 +75,12 @@ func (f *Flow) HandleClientFrame(in frame.Frame) ([][]byte, error) {
6775

6876
switch f.machine.Current() {
6977
case session.PhaseHandshake:
78+
if len(f.stateChecker.Channels) > 0 && in.Header == control.HeaderStateChecker {
79+
if _, err := control.DecodeStateChecker(in); err != nil {
80+
return nil, err
81+
}
82+
return [][]byte{control.EncodeRespondChannelStatus(control.RespondChannelStatusPacket{Channels: f.stateChecker.Channels})}, nil
83+
}
7084
return f.handshake.HandleClientFrame(in)
7185
case session.PhaseLogin:
7286
return f.login.HandleClientFrame(in)

internal/boot/flow_socket_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,43 @@ func TestBootFlowCompletesHandshakeAndLoginOverTCP(t *testing.T) {
7676
}
7777
}
7878

79+
func TestBootFlowRespondsToStateCheckerProbeOverTCP(t *testing.T) {
80+
cfg := testConfig()
81+
cfg.StateChecker = StateCheckerConfig{
82+
Channels: []control.ChannelStatus{{Port: 13000, Status: control.ChannelStatusNormal}},
83+
}
84+
85+
server := startBootTestServer(t, cfg)
86+
client := newBootTestClient(t, server.address())
87+
88+
challenge := client.readFrame(t)
89+
wantChallenge := control.EncodeKeyChallenge(cfg.Handshake.KeyChallenge)
90+
if !bytes.Equal(challenge.Raw, wantChallenge) {
91+
t.Fatalf("unexpected key challenge bytes: got %x want %x", challenge.Raw, wantChallenge)
92+
}
93+
94+
client.writeFrame(t, control.EncodeStateChecker())
95+
96+
respond := client.readFrame(t)
97+
packet, err := control.DecodeRespondChannelStatus(respond.Frame)
98+
if err != nil {
99+
t.Fatalf("decode respond channel status: %v", err)
100+
}
101+
if len(packet.Channels) != 1 {
102+
t.Fatalf("expected 1 channel status entry, got %d", len(packet.Channels))
103+
}
104+
if packet.Channels[0].Port != 13000 {
105+
t.Fatalf("expected channel port 13000, got %d", packet.Channels[0].Port)
106+
}
107+
if packet.Channels[0].Status != control.ChannelStatusNormal {
108+
t.Fatalf("expected channel status %d, got %d", control.ChannelStatusNormal, packet.Channels[0].Status)
109+
}
110+
111+
if got := server.currentPhase(); got != session.PhaseHandshake {
112+
t.Fatalf("expected server phase %q, got %q", session.PhaseHandshake, got)
113+
}
114+
}
115+
79116
func TestBootFlowEntersGameOverTCP(t *testing.T) {
80117
server := startBootTestServer(t, testConfig())
81118
client := newBootTestClient(t, server.address())

internal/minimal/factory.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ func newGameSessionFactoryWithAccountStore(cfg config.Service, store loginticket
145145
}
146146
},
147147
},
148+
StateChecker: boot.StateCheckerConfig{
149+
Channels: []control.ChannelStatus{{Port: int16(advertisedPort), Status: control.ChannelStatusNormal}},
150+
},
148151
WorldEntry: worldentry.Config{
149152
SelectEmpire: func(empire uint8) worldentry.EmpireResult {
150153
if !hasTicket || !isValidEmpire(empire) || hasAnyCharacters(sessionTicket.Characters) {

internal/minimal/factory_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,53 @@ func TestNewGameSessionFactoryReachesGamePhase(t *testing.T) {
226226
}
227227
}
228228

229+
func TestNewGameSessionFactoryRespondsToStateCheckerDuringHandshake(t *testing.T) {
230+
store := loginticket.NewFileStore(t.TempDir())
231+
factory, err := newGameSessionFactory(config.Service{LegacyAddr: ":13000", PublicAddr: "127.0.0.1"}, store)
232+
if err != nil {
233+
t.Fatalf("unexpected game session factory error: %v", err)
234+
}
235+
236+
flow := factory()
237+
startOut, err := flow.Start()
238+
if err != nil {
239+
t.Fatalf("unexpected start error: %v", err)
240+
}
241+
if len(startOut) != 1 {
242+
t.Fatalf("expected 1 handshake start frame, got %d", len(startOut))
243+
}
244+
245+
out, err := flow.HandleClientFrame(decodeSingleFrame(t, control.EncodeStateChecker()))
246+
if err != nil {
247+
t.Fatalf("unexpected state checker handling error: %v", err)
248+
}
249+
if len(out) != 1 {
250+
t.Fatalf("expected 1 state checker frame, got %d", len(out))
251+
}
252+
253+
packet, err := control.DecodeRespondChannelStatus(decodeSingleFrame(t, out[0]))
254+
if err != nil {
255+
t.Fatalf("decode respond channel status: %v", err)
256+
}
257+
if len(packet.Channels) != 1 {
258+
t.Fatalf("expected 1 channel status entry, got %d", len(packet.Channels))
259+
}
260+
if packet.Channels[0].Port != 13000 {
261+
t.Fatalf("expected channel port 13000, got %d", packet.Channels[0].Port)
262+
}
263+
if packet.Channels[0].Status != control.ChannelStatusNormal {
264+
t.Fatalf("expected channel status %d, got %d", control.ChannelStatusNormal, packet.Channels[0].Status)
265+
}
266+
267+
phaseAware, ok := flow.(interface{ CurrentPhase() session.Phase })
268+
if !ok {
269+
t.Fatal("expected game session flow to expose CurrentPhase")
270+
}
271+
if got := phaseAware.CurrentPhase(); got != session.PhaseHandshake {
272+
t.Fatalf("expected phase %q, got %q", session.PhaseHandshake, got)
273+
}
274+
}
275+
229276
func TestNewGameSessionFactoryCreatesACharacterInAnEmptySlot(t *testing.T) {
230277
store := loginticket.NewFileStore(t.TempDir())
231278
if err := store.Issue(loginticket.Ticket{Login: StubLogin, LoginKey: 0x01020304, Empire: 2, Characters: stubCharacters()}); err != nil {

internal/proto/control/control.go

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,23 @@ import (
1010
)
1111

1212
const (
13-
HeaderPong uint16 = 0x0006
14-
HeaderPing uint16 = 0x0007
15-
HeaderPhase uint16 = 0x0008
16-
HeaderKeyResponse uint16 = 0x000A
17-
HeaderKeyChallenge uint16 = 0x000B
18-
HeaderKeyComplete uint16 = 0x000C
13+
HeaderPong uint16 = 0x0006
14+
HeaderPing uint16 = 0x0007
15+
HeaderPhase uint16 = 0x0008
16+
HeaderKeyResponse uint16 = 0x000A
17+
HeaderKeyChallenge uint16 = 0x000B
18+
HeaderKeyComplete uint16 = 0x000C
19+
HeaderStateChecker uint16 = 0x000F
20+
HeaderRespondChannelStatus uint16 = 0x0010
21+
ChannelStatusNormal uint8 = 1
1922

2023
keySize = 32
2124
challengeSize = 32
2225
challengeResponseSize = 32
2326
encryptedTokenSize = 48
2427
nonceSize = 24
28+
channelStatusSize = 3
29+
channelStatusCountSize = 4
2530
keyChallengePayloadLen = keySize + challengeSize + 4
2631
keyResponsePayloadLen = keySize + challengeResponseSize
2732
keyCompletePayloadLen = encryptedTokenSize + nonceSize
@@ -57,6 +62,17 @@ type KeyCompletePacket struct {
5762
Nonce [nonceSize]byte
5863
}
5964

65+
type StateCheckerPacket struct{}
66+
67+
type ChannelStatus struct {
68+
Port int16
69+
Status uint8
70+
}
71+
72+
type RespondChannelStatusPacket struct {
73+
Channels []ChannelStatus
74+
}
75+
6076
func EncodePhase(phase session.Phase) ([]byte, error) {
6177
value, err := encodePhaseValue(phase)
6278
if err != nil {
@@ -99,6 +115,68 @@ func EncodePong() []byte {
99115
return frame.Encode(HeaderPong, nil)
100116
}
101117

118+
func EncodeStateChecker() []byte {
119+
return frame.Encode(HeaderStateChecker, nil)
120+
}
121+
122+
func DecodeStateChecker(f frame.Frame) (StateCheckerPacket, error) {
123+
if f.Header != HeaderStateChecker {
124+
return StateCheckerPacket{}, ErrUnexpectedHeader
125+
}
126+
127+
if len(f.Payload) != 0 {
128+
return StateCheckerPacket{}, ErrInvalidPayload
129+
}
130+
131+
return StateCheckerPacket{}, nil
132+
}
133+
134+
func EncodeRespondChannelStatus(packet RespondChannelStatusPacket) []byte {
135+
payload := make([]byte, channelStatusCountSize+len(packet.Channels)*channelStatusSize)
136+
binary.LittleEndian.PutUint32(payload[:channelStatusCountSize], uint32(len(packet.Channels)))
137+
138+
offset := channelStatusCountSize
139+
for _, channel := range packet.Channels {
140+
binary.LittleEndian.PutUint16(payload[offset:offset+2], uint16(channel.Port))
141+
payload[offset+2] = channel.Status
142+
offset += channelStatusSize
143+
}
144+
145+
return frame.Encode(HeaderRespondChannelStatus, payload)
146+
}
147+
148+
func DecodeRespondChannelStatus(f frame.Frame) (RespondChannelStatusPacket, error) {
149+
if f.Header != HeaderRespondChannelStatus {
150+
return RespondChannelStatusPacket{}, ErrUnexpectedHeader
151+
}
152+
153+
if len(f.Payload) < channelStatusCountSize {
154+
return RespondChannelStatusPacket{}, ErrInvalidPayload
155+
}
156+
157+
count := binary.LittleEndian.Uint32(f.Payload[:channelStatusCountSize])
158+
if count > uint32((len(f.Payload)-channelStatusCountSize)/channelStatusSize) {
159+
return RespondChannelStatusPacket{}, ErrInvalidPayload
160+
}
161+
162+
expectedLen := channelStatusCountSize + int(count)*channelStatusSize
163+
if len(f.Payload) != expectedLen {
164+
return RespondChannelStatusPacket{}, ErrInvalidPayload
165+
}
166+
167+
packet := RespondChannelStatusPacket{Channels: make([]ChannelStatus, int(count))}
168+
offset := channelStatusCountSize
169+
for i := range packet.Channels {
170+
packet.Channels[i] = ChannelStatus{
171+
Port: int16(binary.LittleEndian.Uint16(f.Payload[offset : offset+2])),
172+
Status: f.Payload[offset+2],
173+
}
174+
offset += channelStatusSize
175+
}
176+
177+
return packet, nil
178+
}
179+
102180
func EncodeKeyChallenge(packet KeyChallengePacket) []byte {
103181
payload := make([]byte, keyChallengePayloadLen)
104182
copy(payload[0:keySize], packet.ServerPublicKey[:])

internal/proto/control/control_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,57 @@ func TestEncodePongBuildsAHeaderOnlyControlFrame(t *testing.T) {
138138
}
139139
}
140140

141+
func TestEncodeStateCheckerBuildsAHeaderOnlyControlFrame(t *testing.T) {
142+
want := frame.Encode(HeaderStateChecker, nil)
143+
got := EncodeStateChecker()
144+
145+
if !bytes.Equal(got, want) {
146+
t.Fatalf("unexpected state checker frame bytes: got %x want %x", got, want)
147+
}
148+
}
149+
150+
func TestDecodeStateCheckerAcceptsHeaderOnlyControlFrame(t *testing.T) {
151+
decoder := frame.NewDecoder(1024)
152+
frames, err := decoder.Feed(EncodeStateChecker())
153+
if err != nil {
154+
t.Fatalf("unexpected frame decode error: %v", err)
155+
}
156+
157+
if _, err := DecodeStateChecker(frames[0]); err != nil {
158+
t.Fatalf("unexpected state checker decode error: %v", err)
159+
}
160+
}
161+
162+
func TestEncodeRespondChannelStatusRoundTripsPackedStatusEntries(t *testing.T) {
163+
want := []ChannelStatus{
164+
{Port: 13000, Status: ChannelStatusNormal},
165+
{Port: 13001, Status: 0},
166+
}
167+
168+
raw := EncodeRespondChannelStatus(RespondChannelStatusPacket{Channels: want})
169+
170+
decoder := frame.NewDecoder(1024)
171+
frames, err := decoder.Feed(raw)
172+
if err != nil {
173+
t.Fatalf("unexpected frame decode error: %v", err)
174+
}
175+
176+
packet, err := DecodeRespondChannelStatus(frames[0])
177+
if err != nil {
178+
t.Fatalf("unexpected respond channel status decode error: %v", err)
179+
}
180+
181+
if len(packet.Channels) != len(want) {
182+
t.Fatalf("unexpected channel count: got %d want %d", len(packet.Channels), len(want))
183+
}
184+
185+
for i := range want {
186+
if packet.Channels[i] != want[i] {
187+
t.Fatalf("unexpected channel status %d: got %+v want %+v", i, packet.Channels[i], want[i])
188+
}
189+
}
190+
}
191+
141192
func TestEncodeKeyChallengeBuildsAControlFrame(t *testing.T) {
142193
want := loadHexFixture(t, "key-challenge-frame.hex")
143194

0 commit comments

Comments
 (0)