-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.ts
138 lines (110 loc) · 3.2 KB
/
index.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
import { varnum } from "https://deno.land/std@0.50.0/encoding/binary.ts";
enum PacketType {
SERVERDATA_AUTH = 3,
SERVERDATA_AUTH_RESPONSE = 2,
SERVERDATA_EXECCOMMAND = 2,
SERVERDATA_RESPONSE_VALUE = 0,
}
const nullTerminator = new Uint8Array(0);
class Packet {
constructor(
public ID: number,
public Type: PacketType,
public Body: string,
) { }
toNetwork(): Uint8Array {
const id = toLEUI32(this.ID);
const type = toLEUI32(this.Type);
const body = this.Body && this.Body.length
? new TextEncoder().encode([this.Body, "\u0000"].join())
: new Uint8Array([0]);
const size = toLEUI32(8 + body.byteLength);
return concatArrayBuffers(size, id, type, body, nullTerminator);
}
static fromNetwork(response: Uint8Array, request: Packet): Packet {
const responsePacketIdRaw = response.slice(0, 4);
const responsePacketId = varnum(
responsePacketIdRaw,
{ endian: "little", dataType: "int8" },
);
if (responsePacketId === -1) {
throw new Error("Wrong password!");
}
if (responsePacketId !== request.ID) {
throw new Error("IDs don't match");
}
const typeRaw = response.slice(4, 8);
const type = varnum(
typeRaw,
{ endian: "little", dataType: "int8" },
);
const bodyRaw = response.slice(8, response.byteLength - 2);
const body = new TextDecoder().decode(bodyRaw);
return new Packet(responsePacketId!, type!, body);
}
}
function concatArrayBuffers(...bufs: ArrayBuffer[]) {
const length = bufs.reduce(
(length, { byteLength }) => length + byteLength,
0,
);
const result = new Uint8Array(length);
bufs.reduce((offset, buf) => {
const currentBuf = new Uint8Array(buf);
result.set(currentBuf, offset);
return offset + buf.byteLength;
}, 0);
return result;
}
function toLEUI32(num: number) {
const buf = new ArrayBuffer(4);
const view = new DataView(buf);
view.setInt32(0, num, true);
return buf;
}
type Maybe<T> = T | null;
export class RCONServer {
connection: Maybe<Deno.Conn>;
constructor(
private readonly port: number,
private readonly hostname: string,
private readonly password: string,
) {
this.connection = null;
}
async connect() {
this.connection = await Deno.connect({
port: this.port,
hostname: this.hostname,
transport: "tcp",
});
await this.sendPacket(
new Packet(123, PacketType.SERVERDATA_AUTH, this.password),
);
}
private async sendPacket(request: Packet): Promise<Packet> {
if (!this.connection) {
throw new Error("Connection was not initialized, call .connect() first");
}
this.connection.write(request.toNetwork());
const rawSize = new Uint8Array(4);
await this.connection.read(rawSize);
const size = varnum(
rawSize,
{ endian: "little", dataType: "int8" },
);
const response = new Uint8Array(size!);
await this.connection.read(response);
return Packet.fromNetwork(response, request);
}
async execCommand(command: string): Promise<string> {
const response = await this.sendPacket(
new Packet(
124,
PacketType.SERVERDATA_EXECCOMMAND,
command,
),
);
return response.Body;
}
}