-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathconn.go
254 lines (215 loc) · 6.44 KB
/
conn.go
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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
package rcon
import (
"errors"
"fmt"
"net"
"net/url"
"strconv"
"strings"
"sync"
)
const (
DefaultPort uint16 = 25575
)
// Conn represents a remote RCON connection to a Minecraft server.
//
// The RCON connection allows server administrators to remotely
// execute commands on Minecraft servers.
type Conn struct {
conn net.Conn
mutex sync.Mutex
packets chan *Packet
isClosed bool
}
// Dial connects and authenticates to the specified URL.
//
// The underlying transport layer connection is created along
// with the configured RCON connection.
func Dial(addr, password string) (*Conn, error) {
u, err := url.Parse(addr)
if err != nil {
return nil, err
}
if u.Scheme != "rcon" {
return nil, fmt.Errorf("unsupported scheme '%s'", u.Scheme)
}
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
// we assume that error is due to missing port
host = u.Host
port = strconv.Itoa(int(DefaultPort))
}
c, err := net.Dial("tcp", net.JoinHostPort(host, port))
if err != nil {
return nil, err
}
return NewConn(c, password)
}
// NewConn wraps transport layer connection with RCON configuration.
//
// RCON authentication is performed as part of connection configuration.
// Failed authentication closes the transport layer connection.
func NewConn(c net.Conn, password string) (*Conn, error) {
conn := &Conn{
conn: c,
mutex: sync.Mutex{},
packets: make(chan *Packet),
isClosed: false,
}
conn.start()
if err := conn.authenticate(password); err != nil {
_ = conn.Close()
return nil, fmt.Errorf("authentication failed: %w", err)
}
return conn, nil
}
// SendCommand sends RCON command to server and returns response.
//
// Commands sent are processed sequentially and the next command
// cannot execute until the previous completes. All connection errors
// result in the connection being closed.
func (c *Conn) SendCommand(command string) (string, error) {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.isClosed {
return "", errors.New("connection closed")
}
req := NewPacket(CommandPacket, command)
if err := c.writePacket(req); err != nil {
_ = c.Close()
return "", fmt.Errorf("failed writing packet: %w", err)
}
resp, err := c.readPackets()
if err != nil {
_ = c.Close()
return "", fmt.Errorf("failed reading packets: %w", err)
}
// Responses can be fragmented across multiple packets. Payloads from
// each packet are combined to form the complete command response string.
sb := strings.Builder{}
for _, p := range resp {
sb.WriteString(p.Payload)
}
return sb.String(), nil
}
// IsClosed returns whether the RCON connection is closed.
//
// It is possible that an RCON connection becomes closed due to the
// server hanging up or other connection errors.
func (c *Conn) IsClosed() bool {
return c.isClosed
}
// Close closes the connection.
// Any blocked command executions will be unblocked and return errors.
func (c *Conn) Close() error {
c.isClosed = true
return c.conn.Close()
}
// start begins reading response packets asynchronously from connection.
//
// Read packets are queued in channel for later response processing.
// Errors reading packets result in the connection being closed.
func (c *Conn) start() {
go func() {
for {
packet, err := c.readPacket()
if err != nil {
_ = c.Close()
close(c.packets)
return
}
c.packets <- packet
}
}()
}
// authenticate performs RCON login for connection.
//
// Error is returned if authentication is unsuccessful or
// there are issues reading or writing to the connection.
func (c *Conn) authenticate(password string) error {
req := NewPacket(LoginPacket, password)
if err := c.writePacket(req); err != nil {
return fmt.Errorf("failed writing packet: %w", err)
}
resp, err := c.readPackets()
if err != nil {
return fmt.Errorf("failed reading packets: %w", err)
}
// Check response packet ID for failed login. Packet with
// the same request ID represents successful authentication.
// Packet with ID of -1 represents failed authentication.
if len(resp) != 1 || resp[0].ID != req.ID {
return errors.New("invalid password/response")
}
return nil
}
// readPackets returns slice of response packets following a request.
//
// Since responses can be fragmented across multiple packets, all
// requests are accompanied by a single no-op "termination" packet
// used to indicate that all response packets have been received.
//
// Since Minecraft's server does not support queued request packets
// (which is very annoying), the "termination" packet cannot be sent
// until the original request packet has been processed. Upon receiving
// the first full response packet a "termination" packet is sent allowing
// for the reader to know when all responses have been received for the
// initial request.
//
// The returned response slice contains all packets up to the
// "termination" packet. This group of packets represents the response
// to a single request packet. The ID of each packet will match the
// corresponding request packet ID.
func (c *Conn) readPackets() ([]*Packet, error) {
packets := make([]*Packet, 0)
for p := range c.packets {
// Send termination packet if it has not been sent.
if len(packets) == 0 {
tp := NewPacket(TerminationPacket, "MESSAGE-END")
tb, _ := Marshal(tp)
if _, err := c.conn.Write(tb); err != nil {
return nil, fmt.Errorf("failed writing termination packet: %w", err)
}
}
if p.Payload == TerminalResponse {
break
}
packets = append(packets, p)
}
return packets, nil
}
// readPacket reads from connection and creates next packet.
func (c *Conn) readPacket() (*Packet, error) {
buf := make([]byte, 1)
data := make([]byte, 0)
// The minimum length of a RCON packet is 14 bytes and is terminated
// with two null bytes at the end. Bytes are read one at a time from
// the connection until a complete packet has been read.
for len(data) < 14 || data[len(data)-1] != 0 || data[len(data)-2] != 0 {
_, err := c.conn.Read(buf)
if err != nil {
return nil, err
}
data = append(data, buf[0])
}
p := &Packet{}
if err := Unmarshal(data, p); err != nil {
return nil, err
}
return p, nil
}
// writePacket sends request packet to RCON connection.
//
// Minecraft's server cannot handle queued request packets,
// so it is important to make sure a request is processed
// before making an additional request.
func (c *Conn) writePacket(p *Packet) error {
data, err := Marshal(p)
if err != nil {
return err
}
if _, err := c.conn.Write(data); err != nil {
return err
}
return nil
}