-
Notifications
You must be signed in to change notification settings - Fork 17
/
Copy pathrcon.ts
195 lines (160 loc) · 5.5 KB
/
rcon.ts
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
import { Socket, connect } from "net"
import TypedEmitter from "typed-emitter"
import { decodePacket, encodePacket, PacketType, Packet } from "./packet"
import { createSplitter } from "./splitter"
import { PromiseQueue } from "./queue"
import { EventEmitter } from "events"
export interface RconOptions {
host: string
/** @default 25575 */
port?: number
password: string
/**
* Maximum time for a packet to arrive before an error is thrown
* @default 2000 ms
*/
timeout?: number,
/**
* Maximum number of parallel requests. Most minecraft servers can
* only reliably process one packet at a time.
* @default 1
*/
maxPending?: number
}
const defaultOptions = {
port: 25575,
timeout: 2000,
maxPending: 1
}
interface Events {
connect: () => void
authenticated: () => void
end: () => void
error: (error: any) => void
}
export class Rcon {
static async connect(config: RconOptions): Promise<Rcon> {
const rcon = new Rcon(config)
await rcon.connect()
return rcon
}
private sendQueue: PromiseQueue
private callbacks = new Map<number, (packet: Packet) => void>()
private requestId = 0
config: Required<RconOptions>
emitter = new EventEmitter() as TypedEmitter<Events>
socket: Socket | null = null
authenticated = false
on = this.emitter.on.bind(this.emitter)
once = this.emitter.once.bind(this.emitter)
off = this.emitter.removeListener.bind(this.emitter)
constructor(config: RconOptions) {
this.config = { ...defaultOptions, ...config }
this.sendQueue = new PromiseQueue(this.config.maxPending)
if (config.maxPending) this.emitter.setMaxListeners(config.maxPending)
}
async connect() {
if (this.socket) {
throw new Error("Already connected or connecting")
}
const socket = this.socket = connect({
host: this.config.host,
port: this.config.port
})
try {
await new Promise((resolve, reject) => {
socket.once("error", reject)
socket.on("connect", () => {
socket.off("error", reject)
resolve()
})
})
} catch (error) {
this.socket = null
throw error
}
socket.setNoDelay(true)
socket.on("error", error => this.emitter.emit("error", error))
this.emitter.emit("connect")
this.socket.on("close", () => {
this.emitter.emit("end")
this.sendQueue.pause()
this.socket = null
this.authenticated = false
})
this.socket
.pipe(createSplitter())
.on("data", this.handlePacket.bind(this))
const id = this.requestId
const packet = await this.sendPacket(PacketType.Auth, Buffer.from(this.config.password))
this.sendQueue.resume()
if (packet.id != id || packet.id == -1) {
this.sendQueue.pause()
this.socket.destroy()
this.socket = null
throw new Error("Authentication failed")
}
this.authenticated = true
this.emitter.emit("authenticated")
return this
}
/**
Close the connection to the server.
*/
async end() {
if (!this.socket || this.socket.connecting) {
throw new Error("Not connected")
}
if (!this.socket.writable) throw new Error("End called twice")
this.sendQueue.pause()
this.socket.end()
await new Promise(resolve => this.once("end", resolve))
}
/**
Send a command to the server.
@param command The command that will be executed on the server.
@returns A promise that will be resolved with the command's response from the server.
*/
async send(command: string) {
const payload = await this.sendRaw(Buffer.from(command, "utf-8"))
return payload.toString("utf-8")
}
async sendRaw(buffer: Buffer) {
if (!this.authenticated || !this.socket) throw new Error("Not connected")
const packet = await this.sendPacket(PacketType.Command, buffer)
return packet.payload
}
private async sendPacket(type: PacketType, payload: Buffer) {
const id = this.requestId++
const createSendPromise = () => {
this.socket!.write(encodePacket({ id, type, payload }))
return new Promise<Packet>((resolve, reject) => {
const onEnd = () => (reject(new Error("Connection closed")), clearTimeout(timeout))
this.emitter.on("end", onEnd)
const timeout = setTimeout(() => {
this.off("end", onEnd)
reject(new Error(`Timeout for packet id ${id}`))
}, this.config.timeout)
this.callbacks.set(id, packet => {
this.off("end", onEnd)
clearTimeout(timeout)
resolve(packet)
})
})
}
if (type == PacketType.Auth) {
return createSendPromise()
} else {
return await this.sendQueue.add(createSendPromise)
}
}
private handlePacket(data: Buffer) {
const packet = decodePacket(data)
const id = this.authenticated ? packet.id : this.requestId - 1
const handler = this.callbacks.get(id)
if (handler) {
handler(packet)
this.callbacks.delete(id)
}
}
}