koishi-plugin-minecraft-adapter 1.0.9 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +1 -1
- package/lib/index.js +3 -3
- package/lib/rcon.d.ts +61 -0
- package/lib/rcon.js +215 -0
- package/package.json +1 -2
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.MinecraftAdapter = exports.MinecraftBot = void 0;
|
|
7
7
|
const koishi_1 = require("koishi");
|
|
8
|
-
const
|
|
8
|
+
const rcon_1 = require("./rcon");
|
|
9
9
|
const ws_1 = __importDefault(require("ws"));
|
|
10
10
|
const logger = new koishi_1.Logger('minecraft');
|
|
11
11
|
/**
|
|
@@ -439,7 +439,7 @@ class MinecraftAdapter extends koishi_1.Adapter {
|
|
|
439
439
|
});
|
|
440
440
|
}
|
|
441
441
|
try {
|
|
442
|
-
const rcon = await this.createRconWithTimeout(rconHost, rconPort, config.rconPassword
|
|
442
|
+
const rcon = await this.createRconWithTimeout(rconHost, rconPort, String(config.rconPassword ?? ''), rconTimeout);
|
|
443
443
|
this.rconConnections.set(selfId, rcon);
|
|
444
444
|
bot.rcon = rcon;
|
|
445
445
|
this.rconReconnectAttempts.set(selfId, 0);
|
|
@@ -467,7 +467,7 @@ class MinecraftAdapter extends koishi_1.Adapter {
|
|
|
467
467
|
const timer = setTimeout(() => {
|
|
468
468
|
reject(new Error(`RCON TCP connection timeout after ${timeout}ms to ${host}:${port}`));
|
|
469
469
|
}, timeout);
|
|
470
|
-
|
|
470
|
+
rcon_1.Rcon.connect({ host, port, password, timeout }).then((rcon) => { clearTimeout(timer); resolve(rcon); }, (err) => { clearTimeout(timer); reject(err); });
|
|
471
471
|
});
|
|
472
472
|
}
|
|
473
473
|
scheduleRconReconnect(bot) {
|
package/lib/rcon.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface RconOptions {
|
|
2
|
+
host: string;
|
|
3
|
+
port?: number;
|
|
4
|
+
password: string;
|
|
5
|
+
timeout?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 轻量级 Minecraft RCON 客户端
|
|
9
|
+
* 替换已停止维护的 rcon-client 库,修复认证兼容性问题
|
|
10
|
+
*
|
|
11
|
+
* 主要改进:
|
|
12
|
+
* - 正确处理 Minecraft 服务端的认证响应(兼容 Vanilla/Paper/Spigot/Fabric)
|
|
13
|
+
* - 稳健的 TCP 分包/合包缓冲处理
|
|
14
|
+
* - 独立的 TCP 连接超时和认证超时
|
|
15
|
+
* - 密码类型安全(防止 YAML 解析为非字符串)
|
|
16
|
+
*/
|
|
17
|
+
export declare class Rcon {
|
|
18
|
+
private socket;
|
|
19
|
+
private emitter;
|
|
20
|
+
private buf;
|
|
21
|
+
private nextId;
|
|
22
|
+
private authenticated;
|
|
23
|
+
private authCb;
|
|
24
|
+
private pending;
|
|
25
|
+
private host;
|
|
26
|
+
private port;
|
|
27
|
+
private password;
|
|
28
|
+
private timeout;
|
|
29
|
+
constructor(options: RconOptions);
|
|
30
|
+
static connect(options: RconOptions): Promise<Rcon>;
|
|
31
|
+
on(event: string, listener: (...args: any[]) => void): void;
|
|
32
|
+
once(event: string, listener: (...args: any[]) => void): void;
|
|
33
|
+
off(event: string, listener: (...args: any[]) => void): void;
|
|
34
|
+
connect(): Promise<void>;
|
|
35
|
+
send(command: string): Promise<string>;
|
|
36
|
+
end(): void;
|
|
37
|
+
/**
|
|
38
|
+
* 编码 RCON 数据包
|
|
39
|
+
* 格式: [4:size][4:id][4:type][body][0x00][0x00]
|
|
40
|
+
*/
|
|
41
|
+
private encode;
|
|
42
|
+
/**
|
|
43
|
+
* 从缓冲区中提取并处理完整的 RCON 数据包
|
|
44
|
+
* 正确处理 TCP 分包和合包
|
|
45
|
+
*/
|
|
46
|
+
private drain;
|
|
47
|
+
/**
|
|
48
|
+
* 处理接收到的 RCON 数据包
|
|
49
|
+
*
|
|
50
|
+
* 认证阶段:
|
|
51
|
+
* Minecraft 服务端对认证请求的响应因实现而异:
|
|
52
|
+
* - Vanilla: 仅发送 Auth_Response (type=2, id=请求id 或 -1)
|
|
53
|
+
* - 部分实现: 先发送空 Response_Value (type=0) 再发送 Auth_Response
|
|
54
|
+
*
|
|
55
|
+
* 处理策略:
|
|
56
|
+
* 1. 收到 type=2(Auth_Response)→ 立即判断认证结果
|
|
57
|
+
* 2. 收到 id=-1 → 认证失败(无论 type)
|
|
58
|
+
* 3. 其他包 → 忽略(可能是前导空响应)
|
|
59
|
+
*/
|
|
60
|
+
private onPacket;
|
|
61
|
+
}
|
package/lib/rcon.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Rcon = void 0;
|
|
4
|
+
const net_1 = require("net");
|
|
5
|
+
const events_1 = require("events");
|
|
6
|
+
/**
|
|
7
|
+
* 轻量级 Minecraft RCON 客户端
|
|
8
|
+
* 替换已停止维护的 rcon-client 库,修复认证兼容性问题
|
|
9
|
+
*
|
|
10
|
+
* 主要改进:
|
|
11
|
+
* - 正确处理 Minecraft 服务端的认证响应(兼容 Vanilla/Paper/Spigot/Fabric)
|
|
12
|
+
* - 稳健的 TCP 分包/合包缓冲处理
|
|
13
|
+
* - 独立的 TCP 连接超时和认证超时
|
|
14
|
+
* - 密码类型安全(防止 YAML 解析为非字符串)
|
|
15
|
+
*/
|
|
16
|
+
class Rcon {
|
|
17
|
+
socket = null;
|
|
18
|
+
emitter = new events_1.EventEmitter();
|
|
19
|
+
buf = Buffer.alloc(0);
|
|
20
|
+
nextId = 0;
|
|
21
|
+
authenticated = false;
|
|
22
|
+
// 认证阶段回调(独立于命令回调,避免 id 冲突)
|
|
23
|
+
authCb = null;
|
|
24
|
+
// 命令响应回调
|
|
25
|
+
pending = new Map();
|
|
26
|
+
host;
|
|
27
|
+
port;
|
|
28
|
+
password;
|
|
29
|
+
timeout;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.host = options.host;
|
|
32
|
+
this.port = options.port ?? 25575;
|
|
33
|
+
// 关键修复:强制转为字符串,防止 YAML 将纯数字/布尔密码解析为非 string 类型
|
|
34
|
+
this.password = String(options.password ?? '');
|
|
35
|
+
this.timeout = options.timeout ?? 5000;
|
|
36
|
+
}
|
|
37
|
+
static async connect(options) {
|
|
38
|
+
const rcon = new Rcon(options);
|
|
39
|
+
await rcon.connect();
|
|
40
|
+
return rcon;
|
|
41
|
+
}
|
|
42
|
+
on(event, listener) {
|
|
43
|
+
this.emitter.on(event, listener);
|
|
44
|
+
}
|
|
45
|
+
once(event, listener) {
|
|
46
|
+
this.emitter.once(event, listener);
|
|
47
|
+
}
|
|
48
|
+
off(event, listener) {
|
|
49
|
+
this.emitter.removeListener(event, listener);
|
|
50
|
+
}
|
|
51
|
+
async connect() {
|
|
52
|
+
if (this.socket)
|
|
53
|
+
throw new Error('Already connected');
|
|
54
|
+
// ── Phase 1: TCP 连接 ──
|
|
55
|
+
const socket = (0, net_1.createConnection)({ host: this.host, port: this.port });
|
|
56
|
+
this.socket = socket;
|
|
57
|
+
await new Promise((resolve, reject) => {
|
|
58
|
+
const timer = setTimeout(() => {
|
|
59
|
+
socket.destroy();
|
|
60
|
+
this.socket = null;
|
|
61
|
+
reject(new Error(`RCON: TCP connection to ${this.host}:${this.port} timed out (${this.timeout}ms)`));
|
|
62
|
+
}, this.timeout);
|
|
63
|
+
const onError = (err) => {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
this.socket = null;
|
|
66
|
+
reject(err);
|
|
67
|
+
};
|
|
68
|
+
socket.once('error', onError);
|
|
69
|
+
socket.once('connect', () => {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
socket.removeListener('error', onError);
|
|
72
|
+
resolve();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
socket.setNoDelay(true);
|
|
76
|
+
socket.on('data', (chunk) => {
|
|
77
|
+
this.buf = Buffer.concat([this.buf, chunk]);
|
|
78
|
+
this.drain();
|
|
79
|
+
});
|
|
80
|
+
socket.on('error', (err) => this.emitter.emit('error', err));
|
|
81
|
+
socket.on('close', () => {
|
|
82
|
+
this.authenticated = false;
|
|
83
|
+
this.socket = null;
|
|
84
|
+
this.buf = Buffer.alloc(0);
|
|
85
|
+
for (const [, p] of this.pending) {
|
|
86
|
+
clearTimeout(p.timer);
|
|
87
|
+
p.reject(new Error('Connection closed'));
|
|
88
|
+
}
|
|
89
|
+
this.pending.clear();
|
|
90
|
+
if (this.authCb) {
|
|
91
|
+
clearTimeout(this.authCb.timer);
|
|
92
|
+
this.authCb.reject(new Error('Connection closed during authentication'));
|
|
93
|
+
this.authCb = null;
|
|
94
|
+
}
|
|
95
|
+
this.emitter.emit('end');
|
|
96
|
+
});
|
|
97
|
+
// ── Phase 2: RCON 认证 ──
|
|
98
|
+
const authId = this.nextId++;
|
|
99
|
+
await new Promise((resolve, reject) => {
|
|
100
|
+
const timer = setTimeout(() => {
|
|
101
|
+
this.authCb = null;
|
|
102
|
+
socket.destroy();
|
|
103
|
+
this.socket = null;
|
|
104
|
+
reject(new Error(`RCON: Authentication timed out (${this.timeout}ms)`));
|
|
105
|
+
}, this.timeout);
|
|
106
|
+
this.authCb = { id: authId, resolve, reject, timer };
|
|
107
|
+
// Auth packet: type = 3 (SERVERDATA_AUTH)
|
|
108
|
+
socket.write(this.encode(authId, 3, this.password));
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async send(command) {
|
|
112
|
+
if (!this.authenticated || !this.socket) {
|
|
113
|
+
throw new Error('RCON not connected');
|
|
114
|
+
}
|
|
115
|
+
const id = this.nextId++;
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const timer = setTimeout(() => {
|
|
118
|
+
this.pending.delete(id);
|
|
119
|
+
reject(new Error(`RCON command timed out: ${command}`));
|
|
120
|
+
}, this.timeout);
|
|
121
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
122
|
+
// Command packet: type = 2 (SERVERDATA_EXECCOMMAND)
|
|
123
|
+
this.socket.write(this.encode(id, 2, command));
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
end() {
|
|
127
|
+
this.socket?.destroy();
|
|
128
|
+
}
|
|
129
|
+
// ── RCON 协议编解码 ──
|
|
130
|
+
/**
|
|
131
|
+
* 编码 RCON 数据包
|
|
132
|
+
* 格式: [4:size][4:id][4:type][body][0x00][0x00]
|
|
133
|
+
*/
|
|
134
|
+
encode(id, type, body) {
|
|
135
|
+
const payload = Buffer.from(body, 'utf-8');
|
|
136
|
+
const size = 4 + 4 + payload.length + 2; // id + type + body + 2 null terminators
|
|
137
|
+
const pkt = Buffer.alloc(4 + size); // 4 for size field
|
|
138
|
+
pkt.writeInt32LE(size, 0);
|
|
139
|
+
pkt.writeInt32LE(id, 4);
|
|
140
|
+
pkt.writeInt32LE(type, 8);
|
|
141
|
+
payload.copy(pkt, 12);
|
|
142
|
+
// 末尾 2 字节已由 Buffer.alloc 置 0x00(null terminators)
|
|
143
|
+
return pkt;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 从缓冲区中提取并处理完整的 RCON 数据包
|
|
147
|
+
* 正确处理 TCP 分包和合包
|
|
148
|
+
*/
|
|
149
|
+
drain() {
|
|
150
|
+
while (this.buf.length >= 4) {
|
|
151
|
+
const size = this.buf.readInt32LE(0);
|
|
152
|
+
// 安全校验:size 最小为 10(4 id + 4 type + 2 null),最大为 1MB
|
|
153
|
+
if (size < 10 || size > 0x100000) {
|
|
154
|
+
this.buf = Buffer.alloc(0);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const total = 4 + size;
|
|
158
|
+
if (this.buf.length < total)
|
|
159
|
+
return; // 等待更多数据
|
|
160
|
+
const raw = this.buf.subarray(0, total);
|
|
161
|
+
this.buf = this.buf.subarray(total);
|
|
162
|
+
const id = raw.readInt32LE(4);
|
|
163
|
+
const type = raw.readInt32LE(8);
|
|
164
|
+
// body 位于 header(12字节) 之后、末尾 2 字节 null 之前
|
|
165
|
+
const bodyEnd = Math.max(12, total - 2);
|
|
166
|
+
const body = raw.subarray(12, bodyEnd).toString('utf-8').replace(/\0+$/, '');
|
|
167
|
+
this.onPacket(id, type, body);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* 处理接收到的 RCON 数据包
|
|
172
|
+
*
|
|
173
|
+
* 认证阶段:
|
|
174
|
+
* Minecraft 服务端对认证请求的响应因实现而异:
|
|
175
|
+
* - Vanilla: 仅发送 Auth_Response (type=2, id=请求id 或 -1)
|
|
176
|
+
* - 部分实现: 先发送空 Response_Value (type=0) 再发送 Auth_Response
|
|
177
|
+
*
|
|
178
|
+
* 处理策略:
|
|
179
|
+
* 1. 收到 type=2(Auth_Response)→ 立即判断认证结果
|
|
180
|
+
* 2. 收到 id=-1 → 认证失败(无论 type)
|
|
181
|
+
* 3. 其他包 → 忽略(可能是前导空响应)
|
|
182
|
+
*/
|
|
183
|
+
onPacket(id, type, body) {
|
|
184
|
+
if (!this.authenticated && this.authCb) {
|
|
185
|
+
// Auth_Response (type=2) 或 id=-1 → 最终认证结果
|
|
186
|
+
if (type === 2 || id === -1) {
|
|
187
|
+
const cb = this.authCb;
|
|
188
|
+
this.authCb = null;
|
|
189
|
+
clearTimeout(cb.timer);
|
|
190
|
+
if (id === -1) {
|
|
191
|
+
cb.reject(new Error('RCON authentication failed: incorrect password'));
|
|
192
|
+
}
|
|
193
|
+
else if (id === cb.id) {
|
|
194
|
+
this.authenticated = true;
|
|
195
|
+
this.emitter.emit('authenticated');
|
|
196
|
+
cb.resolve();
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
cb.reject(new Error(`RCON authentication failed: unexpected response id ${id}`));
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
// type != 2 且 id != -1 → 前导空响应包,忽略
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// ── 命令响应 ──
|
|
207
|
+
const cb = this.pending.get(id);
|
|
208
|
+
if (cb) {
|
|
209
|
+
this.pending.delete(id);
|
|
210
|
+
clearTimeout(cb.timer);
|
|
211
|
+
cb.resolve(body);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
exports.Rcon = Rcon;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-minecraft-adapter",
|
|
3
3
|
"description": "Minecraft adapter for Koishi based on QueQiao V2 protocol",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.10",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
"koishi": "^4.17.9"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"rcon-client": "^4.2.5",
|
|
26
25
|
"ws": "^8.18.0"
|
|
27
26
|
},
|
|
28
27
|
"devDependencies": {
|