jsql-neo 5.4.2 → 5.5.0
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/LICENSE +202 -202
- package/README.md +8097 -7971
- package/bin/jsql +320 -320
- package/bin/jsql-server +90 -90
- package/bin/jsql-server-control.js +177 -177
- package/index.d.ts +385 -357
- package/index.js +124 -118
- package/lib/ast.js +214 -0
- package/lib/btree.js +413 -413
- package/lib/cache.js +328 -328
- package/lib/client.js +125 -125
- package/lib/database.js +1744 -1744
- package/lib/date-types.js +182 -182
- package/lib/errors.js +89 -89
- package/lib/jsql_format.js +497 -497
- package/lib/migrate.js +254 -254
- package/lib/mod.js +292 -292
- package/lib/mongo_server.js +607 -607
- package/lib/multiserver.js +140 -140
- package/lib/mysql_compat.js +413 -413
- package/lib/mysql_server.js +1237 -1237
- package/lib/native_client.js +732 -732
- package/lib/nedb_compat.js +506 -506
- package/lib/pg_server.js +864 -864
- package/lib/plugin.js +34 -34
- package/lib/query.js +815 -815
- package/lib/redis_server.js +571 -571
- package/lib/sql.js +3460 -2925
- package/lib/sqlite_compat.js +323 -323
- package/lib/sqlite_worker.js +264 -264
- package/lib/table.js +1304 -1304
- package/lib/tui.js +502 -502
- package/lib/wasm.js +1 -1
- package/lib/wasm_client.js +718 -718
- package/lib/web_ui.js +257 -257
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/Cargo.lock +455 -455
- package/nativesrc/jsql-neo-core/Cargo.toml +24 -24
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +455 -455
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +168 -168
- package/nativesrc/jsql-neo-core/src/engine/mod.rs +62 -62
- package/nativesrc/jsql-neo-core/src/engine/table.rs +700 -700
- package/nativesrc/jsql-neo-core/src/lib.rs +2 -2
- package/nativesrc/jsql-neo-core/src/storage/mod.rs +2 -2
- package/nativesrc/jsql-neo-core/src/storage/persistent.rs +1 -1
- package/nativesrc/jsql-neo-core/src/storage/wal.rs +85 -85
- package/nativesrc/jsql-neo-core/src/types.rs +93 -93
- package/nativesrc/jsql-neo-native/Cargo.lock +606 -606
- package/nativesrc/jsql-neo-native/Cargo.toml +16 -16
- package/nativesrc/jsql-neo-native/build.rs +3 -3
- package/nativesrc/jsql-neo-native/package.json +7 -7
- package/nativesrc/jsql-neo-native/src/lib.rs +269 -269
- package/nativesrc/jsql-neo-wasm/Cargo.lock +465 -465
- package/nativesrc/jsql-neo-wasm/Cargo.toml +15 -15
- package/nativesrc/jsql-neo-wasm/src/lib.rs +447 -447
- package/package.json +87 -86
- package/postinstall.js +158 -158
- package/test/btree.test.js +128 -128
- package/test/coverage.js +187 -187
- package/test/join.test.js +110 -110
- package/test/native.test.js +158 -158
- package/test/regress-5.1.0.js +271 -271
- package/test/smoke.js +58 -58
- package/test/sql-parser.test.js +273 -0
- package/test/wasm.test.js +110 -110
- package/wasm/browser.d.ts +89 -89
- package/wasm/browser.mjs +569 -569
- package/wasm/browser_bg.mjs +597 -597
- package/wasm/jsql_neo_wasm.d.ts +38 -38
- package/wasm/jsql_neo_wasm.js +520 -520
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +26 -26
- package/wasm/package.json +11 -11
package/lib/mysql_server.js
CHANGED
|
@@ -1,1237 +1,1237 @@
|
|
|
1
|
-
// © Vexify 2026 All Rights Reserved.
|
|
2
|
-
/**
|
|
3
|
-
* MySQL 协议服务端 — TCP 3306
|
|
4
|
-
* 实现 MySQL 握手/认证/命令循环,任何 mysql/mysql2 客户端可直接连接。
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const net = require('net');
|
|
8
|
-
const crypto = require('crypto');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
const { executeSQL, parseSQL, splitStatements, applyParams } = require('./sql');
|
|
12
|
-
const Database = require('./database');
|
|
13
|
-
|
|
14
|
-
const SERVER_VERSION = '8.0.0-jsql-neo';
|
|
15
|
-
const CLIENT_PROTOCOL_41 = 0x00000001 << 9;
|
|
16
|
-
const CLIENT_SECURE_CONNECTION = 0x00008000;
|
|
17
|
-
const CLIENT_PLUGIN_AUTH = 0x00080000;
|
|
18
|
-
const CLIENT_CONNECT_WITH_DB = 0x00000008;
|
|
19
|
-
const CLIENT_LONG_PASSWORD = 0x00000001;
|
|
20
|
-
const CLIENT_TRANSACTIONS = 0x00002000;
|
|
21
|
-
const CLIENT_MULTI_STATEMENTS = 0x00010000;
|
|
22
|
-
const CLIENT_MULTI_RESULTS = 0x00020000;
|
|
23
|
-
const CLIENT_LONG_FLAG = 0x00000004;
|
|
24
|
-
const CLIENT_DEPRECATE_EOF = 0x01000000;
|
|
25
|
-
const SERVER_STATUS_AUTOCOMMIT = 0x0002;
|
|
26
|
-
const CHARSET_UTF8 = 0x21;
|
|
27
|
-
|
|
28
|
-
const MYSQL_TYPE_TINY = 1, MYSQL_TYPE_LONG = 3, MYSQL_TYPE_LONGLONG = 8,
|
|
29
|
-
MYSQL_TYPE_DATE = 10, MYSQL_TYPE_DATETIME = 12, MYSQL_TYPE_DOUBLE = 5,
|
|
30
|
-
MYSQL_TYPE_STRING = 254, MYSQL_TYPE_VAR_STRING = 253, MYSQL_TYPE_BLOB = 252,
|
|
31
|
-
MYSQL_TYPE_JSON = 245, MYSQL_TYPE_NULL = 6;
|
|
32
|
-
|
|
33
|
-
function encodeLenenc(value) {
|
|
34
|
-
if (typeof value === 'bigint') value = Number(value);
|
|
35
|
-
if (value === null) return Buffer.from([0xfb]);
|
|
36
|
-
if (value < 0xfb) return Buffer.from([value]);
|
|
37
|
-
if (value <= 0xffff) {
|
|
38
|
-
const b = Buffer.alloc(3);
|
|
39
|
-
b[0] = 0xfc; b.writeUInt16LE(value, 1);
|
|
40
|
-
return b;
|
|
41
|
-
}
|
|
42
|
-
if (value <= 0xffffff) {
|
|
43
|
-
const b = Buffer.alloc(4);
|
|
44
|
-
b[0] = 0xfd; b.writeUIntLE(value, 1, 3);
|
|
45
|
-
return b;
|
|
46
|
-
}
|
|
47
|
-
const b = Buffer.alloc(9);
|
|
48
|
-
b[0] = 0xfe; b.writeUInt32LE(value, 1); b.writeUInt32LE(Math.floor(value / 4294967296), 5);
|
|
49
|
-
return b;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function encodeLenencString(str) {
|
|
53
|
-
const buf = Buffer.from(String(str), 'utf8');
|
|
54
|
-
return Buffer.concat([encodeLenenc(buf.length), buf]);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function parseLenenc(buf, offset) {
|
|
58
|
-
const first = buf[offset];
|
|
59
|
-
if (first < 0xfb) return { value: first, size: 1 };
|
|
60
|
-
if (first === 0xfb) return { value: null, size: 1 };
|
|
61
|
-
if (first === 0xfc) return { value: buf.readUInt16LE(offset + 1), size: 3 };
|
|
62
|
-
if (first === 0xfd) return { value: buf.readUIntLE(offset + 1, 3), size: 4 };
|
|
63
|
-
return { value: buf.readUInt32LE(offset + 1), size: 5 };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function readLenenc(buf, offset) {
|
|
67
|
-
const { value, size } = parseLenenc(buf, offset);
|
|
68
|
-
if (value === null) return { value: null, off: offset + size };
|
|
69
|
-
const str = buf.slice(offset + size, offset + size + value).toString('utf8');
|
|
70
|
-
return { value: str, off: offset + size + value };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
class PacketBuilder {
|
|
74
|
-
constructor() {
|
|
75
|
-
this.bufs = [];
|
|
76
|
-
}
|
|
77
|
-
byte(v) { this.bufs.push(Buffer.from([v & 0xff])); return this; }
|
|
78
|
-
int16(v) { const b = Buffer.alloc(2); b.writeUInt16LE(v & 0xffff); this.bufs.push(b); return this; }
|
|
79
|
-
int32(v) { const b = Buffer.alloc(4); b.writeUInt32LE(v >>> 0); this.bufs.push(b); return this; }
|
|
80
|
-
raw(buf) { this.bufs.push(buf); return this; }
|
|
81
|
-
string(str) { this.bufs.push(Buffer.from(str, 'utf8')); return this; }
|
|
82
|
-
nul(str) { this.bufs.push(Buffer.concat([Buffer.from(String(str), 'utf8'), Buffer.from([0])])); return this; }
|
|
83
|
-
bytes(list) { this.bufs.push(Buffer.from(list)); return this; }
|
|
84
|
-
build() { return Buffer.concat(this.bufs); }
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function handshakePacket(connectionId, seed) {
|
|
88
|
-
const b = new PacketBuilder();
|
|
89
|
-
b.byte(10);
|
|
90
|
-
b.nul(SERVER_VERSION);
|
|
91
|
-
b.int32(connectionId);
|
|
92
|
-
b.bytes(seed.slice(0, 8));
|
|
93
|
-
b.byte(0);
|
|
94
|
-
const caps = CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION | CLIENT_PLUGIN_AUTH | CLIENT_CONNECT_WITH_DB | CLIENT_LONG_PASSWORD | CLIENT_TRANSACTIONS | CLIENT_MULTI_STATEMENTS | CLIENT_MULTI_RESULTS | CLIENT_LONG_FLAG;
|
|
95
|
-
b.int16(caps & 0xffff);
|
|
96
|
-
b.byte(CHARSET_UTF8);
|
|
97
|
-
b.int16(SERVER_STATUS_AUTOCOMMIT);
|
|
98
|
-
b.int16((caps >>> 16) & 0xffff);
|
|
99
|
-
b.byte(21);
|
|
100
|
-
b.bytes([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
|
101
|
-
b.bytes(seed.slice(8, 20));
|
|
102
|
-
b.byte(0);
|
|
103
|
-
b.nul('mysql_native_password');
|
|
104
|
-
return b.build();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* 生成握手 seed:使用可打印 ASCII(33-126),避开 0x00。
|
|
109
|
-
* 老客户端按 C 字符串读取 seed,若含 \0 会提前截断导致握手失败/断连。
|
|
110
|
-
*/
|
|
111
|
-
function genSeed(len = 20) {
|
|
112
|
-
const out = Buffer.alloc(len);
|
|
113
|
-
for (let i = 0; i < len; i++) {
|
|
114
|
-
out[i] = 33 + Math.floor(Math.random() * 94);
|
|
115
|
-
}
|
|
116
|
-
return out;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function okPacket(affectedRows = 0, insertId = 0, status = SERVER_STATUS_AUTOCOMMIT) { const b = new PacketBuilder();
|
|
120
|
-
b.byte(0x00);
|
|
121
|
-
b.raw(encodeLenenc(affectedRows));
|
|
122
|
-
b.raw(encodeLenenc(insertId));
|
|
123
|
-
b.int16(status);
|
|
124
|
-
b.int16(0);
|
|
125
|
-
return b.build();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function eofPacket() {
|
|
129
|
-
const b = new PacketBuilder();
|
|
130
|
-
b.byte(0xfe);
|
|
131
|
-
b.int16(0);
|
|
132
|
-
b.int16(SERVER_STATUS_AUTOCOMMIT);
|
|
133
|
-
return b.build();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function errPacket(errno, message, sqlState = 'HY000') {
|
|
137
|
-
const b = new PacketBuilder();
|
|
138
|
-
b.byte(0xff);
|
|
139
|
-
b.int16(errno);
|
|
140
|
-
b.byte(0x23);
|
|
141
|
-
b.string(sqlState);
|
|
142
|
-
b.string(String(message).slice(0, 200));
|
|
143
|
-
return b.build();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function toMysqlErrno(e) {
|
|
147
|
-
if (e && typeof e.code === 'number') return e.code;
|
|
148
|
-
const m = e && e.message ? String(e.message).match(/^(ER_[A-Z_]+)/) : null;
|
|
149
|
-
if (m) {
|
|
150
|
-
const known = {
|
|
151
|
-
ER_DUP_ENTRY: 1062,
|
|
152
|
-
ER_NO_SUCH_TABLE: 1146,
|
|
153
|
-
ER_TABLE_EXISTS: 1050,
|
|
154
|
-
ER_TABLE_EXISTS_ERROR: 1050,
|
|
155
|
-
ER_PARSE_ERROR: 1064,
|
|
156
|
-
ER_BAD_FIELD_ERROR: 1054,
|
|
157
|
-
ER_BAD_NULL_ERROR: 1048,
|
|
158
|
-
ER_ACCESS_DENIED_ERROR: 1045,
|
|
159
|
-
ER_DBACCESS_DENIED_ERROR: 1044,
|
|
160
|
-
ER_BAD_DB_ERROR: 1049,
|
|
161
|
-
ER_WRONG_DB_NAME: 1102,
|
|
162
|
-
ER_WRONG_TABLE_NAME: 1103,
|
|
163
|
-
ER_WRONG_COLUMN_NAME: 1166,
|
|
164
|
-
ER_DATA_TOO_LONG: 1406,
|
|
165
|
-
ER_OUT_OF_RANGE: 1264,
|
|
166
|
-
ER_CHECK_CONSTRAINT: 3819,
|
|
167
|
-
ER_NO_DEFAULT_FOR_FIELD: 1364,
|
|
168
|
-
ER_CANT_DROP_FIELD_OR_KEY: 1091,
|
|
169
|
-
ER_CANT_DROP_DATABASE: 1008,
|
|
170
|
-
ER_EMPTY_QUERY: 1065,
|
|
171
|
-
ER_UNKNOWN_TABLE: 1109,
|
|
172
|
-
ER_NON_UNIQ_ERROR: 1052,
|
|
173
|
-
ER_WRONG_FIELD_WITH_GROUP: 1055,
|
|
174
|
-
ER_WRONG_VALUE_COUNT_ON_ROW: 1136,
|
|
175
|
-
ER_MISSING_TABLE: 1052,
|
|
176
|
-
ER_SP_DOES_NOT_EXIST: 1305,
|
|
177
|
-
ER_NOT_SUPPORTED_YET: 1235,
|
|
178
|
-
ER_LOCK_DEADLOCK: 1213,
|
|
179
|
-
ER_LOCK_WAIT_TIMEOUT: 1205,
|
|
180
|
-
ER_UNKNOWN_ERROR: 1105,
|
|
181
|
-
};
|
|
182
|
-
if (known[m[1]]) return known[m[1]];
|
|
183
|
-
}
|
|
184
|
-
return 1105;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function columnDefinition(column) {
|
|
188
|
-
const b = new PacketBuilder();
|
|
189
|
-
b.raw(encodeLenencString('def'));
|
|
190
|
-
b.raw(encodeLenencString(column.table || ''));
|
|
191
|
-
b.raw(encodeLenencString(column.table || ''));
|
|
192
|
-
b.raw(encodeLenencString(column.table || ''));
|
|
193
|
-
b.raw(encodeLenencString(column.name || ''));
|
|
194
|
-
b.raw(encodeLenencString(column.name || ''));
|
|
195
|
-
b.raw(encodeLenenc(0x0c));
|
|
196
|
-
b.int16(column.charset || CHARSET_UTF8);
|
|
197
|
-
b.int32(column.length || 1024);
|
|
198
|
-
b.byte(column.type !== undefined ? column.type : MYSQL_TYPE_VAR_STRING);
|
|
199
|
-
b.int16(0);
|
|
200
|
-
b.byte(0);
|
|
201
|
-
b.int16(0);
|
|
202
|
-
return b.build();
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function columnTypeFromSchema(def) {
|
|
206
|
-
const type = def && def.type ? String(def.type).toLowerCase() : 'string';
|
|
207
|
-
if (type === 'integer' || type === 'int' || type === 'bigint') return MYSQL_TYPE_LONG;
|
|
208
|
-
if (type === 'number' || type === 'float' || type === 'double' || type === 'real') return MYSQL_TYPE_DOUBLE;
|
|
209
|
-
if (type === 'boolean' || type === 'bool') return MYSQL_TYPE_TINY;
|
|
210
|
-
if (type === 'date') return MYSQL_TYPE_DATE;
|
|
211
|
-
if (type === 'datetime' || type === 'timestamp') return MYSQL_TYPE_DATETIME;
|
|
212
|
-
if (type === 'object' || type === 'array') return MYSQL_TYPE_JSON;
|
|
213
|
-
if (type === 'binary') return MYSQL_TYPE_BLOB;
|
|
214
|
-
return MYSQL_TYPE_VAR_STRING;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function resultSetPacket(result, tableSchema, baseSeq, rawRow) {
|
|
218
|
-
const packets = [];
|
|
219
|
-
let sequence = baseSeq || 0;
|
|
220
|
-
const push = (buf) => {
|
|
221
|
-
const header = Buffer.alloc(4);
|
|
222
|
-
header.writeUIntLE(buf.length, 0, 3);
|
|
223
|
-
header[3] = sequence;
|
|
224
|
-
sequence++;
|
|
225
|
-
packets.push(header);
|
|
226
|
-
packets.push(buf);
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
const inferType = (name, value) => {
|
|
230
|
-
if (tableSchema && tableSchema[name]) return columnTypeFromSchema(tableSchema[name]);
|
|
231
|
-
if (value === null || value === undefined) return MYSQL_TYPE_NULL;
|
|
232
|
-
if (typeof value === 'number') return Number.isInteger(value) ? MYSQL_TYPE_LONGLONG : MYSQL_TYPE_DOUBLE;
|
|
233
|
-
if (typeof value === 'boolean') return MYSQL_TYPE_TINY;
|
|
234
|
-
if (typeof value === 'object') return MYSQL_TYPE_JSON;
|
|
235
|
-
return MYSQL_TYPE_VAR_STRING;
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
push(encodeLenenc((result.columns || []).length));
|
|
239
|
-
const sampleRow = (result.rows && result.rows[0]) || null;
|
|
240
|
-
for (let ci = 0; ci < (result.columns || []).length; ci++) {
|
|
241
|
-
const name = result.columns[ci];
|
|
242
|
-
let sample;
|
|
243
|
-
if (sampleRow !== null) {
|
|
244
|
-
if (Array.isArray(sampleRow)) sample = sampleRow[ci];
|
|
245
|
-
else sample = sampleRow[name];
|
|
246
|
-
}
|
|
247
|
-
const col = {
|
|
248
|
-
name,
|
|
249
|
-
table: result.table || '',
|
|
250
|
-
type: inferType(name, sample),
|
|
251
|
-
length: 1024,
|
|
252
|
-
};
|
|
253
|
-
push(columnDefinition(col));
|
|
254
|
-
}
|
|
255
|
-
push(eofPacket());
|
|
256
|
-
|
|
257
|
-
for (const row of result.rows || []) {
|
|
258
|
-
const parts = [];
|
|
259
|
-
for (let i = 0; i < (result.columns || []).length; i++) {
|
|
260
|
-
const v = row[i];
|
|
261
|
-
if (v === null || v === undefined) {
|
|
262
|
-
parts.push(Buffer.from([0xfb]));
|
|
263
|
-
} else if (typeof v === 'boolean') {
|
|
264
|
-
parts.push(encodeLenencString(v ? '1' : '0'));
|
|
265
|
-
} else if (typeof v === 'object') {
|
|
266
|
-
parts.push(encodeLenencString(JSON.stringify(v)));
|
|
267
|
-
} else {
|
|
268
|
-
parts.push(encodeLenencString(v));
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
push(Buffer.concat(parts));
|
|
272
|
-
}
|
|
273
|
-
push(eofPacket());
|
|
274
|
-
return { packets, sequence };
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function binaryResultSetPacket(result, tableSchema, baseSeq) {
|
|
278
|
-
const packets = [];
|
|
279
|
-
let sequence = baseSeq || 0;
|
|
280
|
-
const push = (buf) => {
|
|
281
|
-
const header = Buffer.alloc(4);
|
|
282
|
-
header.writeUIntLE(buf.length, 0, 3);
|
|
283
|
-
header[3] = sequence;
|
|
284
|
-
sequence++;
|
|
285
|
-
packets.push(header);
|
|
286
|
-
packets.push(buf);
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
const inferType = (name, value) => {
|
|
290
|
-
if (tableSchema && tableSchema[name]) return columnTypeFromSchema(tableSchema[name]);
|
|
291
|
-
if (value === null || value === undefined) return MYSQL_TYPE_NULL;
|
|
292
|
-
if (typeof value === 'number') return Number.isInteger(value) ? MYSQL_TYPE_LONGLONG : MYSQL_TYPE_DOUBLE;
|
|
293
|
-
if (typeof value === 'boolean') return MYSQL_TYPE_TINY;
|
|
294
|
-
if (typeof value === 'object') return MYSQL_TYPE_JSON;
|
|
295
|
-
return MYSQL_TYPE_VAR_STRING;
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
const cols = result.columns || [];
|
|
299
|
-
push(encodeLenenc(cols.length));
|
|
300
|
-
const sampleRow = (result.rows && result.rows[0]) || null;
|
|
301
|
-
const types = [];
|
|
302
|
-
for (let ci = 0; ci < cols.length; ci++) {
|
|
303
|
-
let sample;
|
|
304
|
-
if (sampleRow !== null) {
|
|
305
|
-
if (Array.isArray(sampleRow)) sample = sampleRow[ci];
|
|
306
|
-
else sample = sampleRow[cols[ci]];
|
|
307
|
-
}
|
|
308
|
-
const t = inferType(cols[ci], sample);
|
|
309
|
-
types.push(t);
|
|
310
|
-
push(columnDefinition({ name: cols[ci], table: result.table || '', type: t, length: 1024 }));
|
|
311
|
-
}
|
|
312
|
-
push(eofPacket());
|
|
313
|
-
|
|
314
|
-
const encodeValue = (v, type) => {
|
|
315
|
-
if (v === null || v === undefined) return null;
|
|
316
|
-
if (type === MYSQL_TYPE_LONG || type === MYSQL_TYPE_LONGLONG || type === MYSQL_TYPE_DOUBLE) {
|
|
317
|
-
const num = Number(v);
|
|
318
|
-
if (type === MYSQL_TYPE_DOUBLE) {
|
|
319
|
-
if (!Number.isFinite(num)) return encodeLenencString(String(v));
|
|
320
|
-
const b = Buffer.alloc(8); b.writeDoubleLE(num, 0); return b;
|
|
321
|
-
}
|
|
322
|
-
if (Number.isInteger(num) && num <= 2147483647 && num >= -2147483648) {
|
|
323
|
-
const b = Buffer.alloc(4); b.writeInt32LE(num, 0); return b;
|
|
324
|
-
}
|
|
325
|
-
if (!Number.isFinite(num)) return encodeLenencString(String(v));
|
|
326
|
-
const b = Buffer.alloc(8); b.writeBigInt64LE(BigInt(Math.trunc(num)), 0); return b;
|
|
327
|
-
}
|
|
328
|
-
if (type === MYSQL_TYPE_TINY) {
|
|
329
|
-
const b = Buffer.alloc(1); b.writeInt8(v ? 1 : 0, 0); return b;
|
|
330
|
-
}
|
|
331
|
-
if (type === MYSQL_TYPE_DATE || type === MYSQL_TYPE_DATETIME) {
|
|
332
|
-
const s = String(v);
|
|
333
|
-
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2}))?/);
|
|
334
|
-
if (!m) return encodeLenencString(String(v));
|
|
335
|
-
const hasTime = !!m[4];
|
|
336
|
-
const buf = hasTime ? Buffer.alloc(8) : Buffer.alloc(5);
|
|
337
|
-
let off = 0;
|
|
338
|
-
buf[off++] = hasTime ? 7 : 4;
|
|
339
|
-
buf.writeUInt16LE(parseInt(m[1], 10), off); off += 2;
|
|
340
|
-
buf[off++] = parseInt(m[2], 10);
|
|
341
|
-
buf[off++] = parseInt(m[3], 10);
|
|
342
|
-
if (hasTime) {
|
|
343
|
-
buf[off++] = parseInt(m[4], 10);
|
|
344
|
-
buf[off++] = parseInt(m[5], 10);
|
|
345
|
-
buf[off++] = parseInt(m[6], 10);
|
|
346
|
-
}
|
|
347
|
-
return buf;
|
|
348
|
-
}
|
|
349
|
-
return encodeLenencString(typeof v === 'object' ? JSON.stringify(v) : v);
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
for (const row of result.rows || []) {
|
|
353
|
-
const parts = [Buffer.from([0x00])];
|
|
354
|
-
const nb = Buffer.alloc(Math.ceil(cols.length / 8));
|
|
355
|
-
const encoded = [];
|
|
356
|
-
for (let i = 0; i < cols.length; i++) {
|
|
357
|
-
const v = Array.isArray(row) ? row[i] : row[cols[i]];
|
|
358
|
-
if (v === null || v === undefined) {
|
|
359
|
-
nb[Math.floor(i / 8)] |= (1 << (i % 8));
|
|
360
|
-
encoded.push(null);
|
|
361
|
-
} else {
|
|
362
|
-
encoded.push(encodeValue(v, types[i]));
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
parts.push(nb);
|
|
366
|
-
for (const e of encoded) if (e !== null) parts.push(e);
|
|
367
|
-
push(Buffer.concat(parts));
|
|
368
|
-
}
|
|
369
|
-
push(eofPacket());
|
|
370
|
-
return { packets, sequence };
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
class MysqlConnection {
|
|
374
|
-
constructor(socket, server) {
|
|
375
|
-
this.socket = socket;
|
|
376
|
-
this.server = server;
|
|
377
|
-
this.connectionId = ++server._connectionCounter;
|
|
378
|
-
this.seed = genSeed(20);
|
|
379
|
-
this.buffer = Buffer.alloc(0);
|
|
380
|
-
this.sequence = 0;
|
|
381
|
-
this.authenticated = false;
|
|
382
|
-
this.user = null;
|
|
383
|
-
this.authFails = 0;
|
|
384
|
-
this.multiStatements = false;
|
|
385
|
-
this._stmts = new Map();
|
|
386
|
-
this._stmtSeq = 0;
|
|
387
|
-
this.session = {
|
|
388
|
-
lastInsertId: 0,
|
|
389
|
-
rowCount: 0,
|
|
390
|
-
foundRows: 0,
|
|
391
|
-
connectionId: server._connectionCounter,
|
|
392
|
-
currentDb: null,
|
|
393
|
-
sysvars: {},
|
|
394
|
-
};
|
|
395
|
-
if (server.handshakeTimeout > 0) {
|
|
396
|
-
this._authTimer = setTimeout(() => {
|
|
397
|
-
if (!this.authenticated) {
|
|
398
|
-
this.server._onSecurityEvent({ type: 'auth-timeout', user: this.user, remote: socket.remoteAddress });
|
|
399
|
-
socket.destroy();
|
|
400
|
-
}
|
|
401
|
-
}, server.handshakeTimeout);
|
|
402
|
-
this._authTimer.unref();
|
|
403
|
-
}
|
|
404
|
-
this.socket.on('data', chunk => this._onData(chunk));
|
|
405
|
-
this.socket.on('error', () => {});
|
|
406
|
-
this._armIdleTimer();
|
|
407
|
-
this._send(handshakePacket(this.connectionId, this.seed));
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
_armIdleTimer() {
|
|
411
|
-
if (this._idleTimer) { clearTimeout(this._idleTimer); this._idleTimer = null; }
|
|
412
|
-
const ttl = this.server.idleTimeout;
|
|
413
|
-
if (!this.authenticated) return;
|
|
414
|
-
if (ttl <= 0) return;
|
|
415
|
-
this._idleTimer = setTimeout(() => {
|
|
416
|
-
this.server._onSecurityEvent({ type: 'idle-timeout', user: this.user, remote: this.socket.remoteAddress });
|
|
417
|
-
this.socket.destroy();
|
|
418
|
-
}, ttl);
|
|
419
|
-
this._idleTimer.unref();
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
_send(payload) {
|
|
423
|
-
if (!this.socket.writable) return;
|
|
424
|
-
const header = Buffer.alloc(4);
|
|
425
|
-
header.writeUIntLE(payload.length, 0, 3);
|
|
426
|
-
header[3] = this.sequence;
|
|
427
|
-
this.sequence = (this.sequence + 1) & 0xff;
|
|
428
|
-
this.socket.write(Buffer.concat([header, payload]));
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
_onData(chunk) {
|
|
432
|
-
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
433
|
-
this._armIdleTimer();
|
|
434
|
-
if (this.buffer.length > this.server.maxPacketSize + 4) {
|
|
435
|
-
this._malicious('packet exceeds maxPacketSize (' + this.server.maxPacketSize + ')');
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
while (true) {
|
|
439
|
-
if (this.buffer.length < 4) return;
|
|
440
|
-
const len = this.buffer.readUIntLE(0, 3);
|
|
441
|
-
if (len > this.server.maxPacketSize) {
|
|
442
|
-
this._malicious('packet length ' + len + ' exceeds maxPacketSize');
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
if (this.buffer.length < 4 + len) return;
|
|
446
|
-
const seq = this.buffer[3];
|
|
447
|
-
const payload = this.buffer.slice(4, 4 + len);
|
|
448
|
-
this.buffer = this.buffer.slice(4 + len);
|
|
449
|
-
this._handlePacket(payload, seq);
|
|
450
|
-
if (this.socket.destroyed) return;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
_malicious(reason) {
|
|
455
|
-
this.socket.destroy();
|
|
456
|
-
this.server._onSecurityEvent({ type: 'malicious', reason, user: this.user, remote: this.socket.remoteAddress });
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
_handlePacket(payload, cmdSeq) {
|
|
460
|
-
if (!this.authenticated) {
|
|
461
|
-
this.sequence = (cmdSeq + 1) & 0xff;
|
|
462
|
-
this._handleAuth(payload);
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
this.sequence = (cmdSeq + 1) & 0xff;
|
|
466
|
-
const cmd = payload[0];
|
|
467
|
-
const body = payload.slice(1);
|
|
468
|
-
try {
|
|
469
|
-
switch (cmd) {
|
|
470
|
-
case 0x01: this.socket.end(); break; // COM_QUIT
|
|
471
|
-
case 0x02: { // COM_INIT_DB
|
|
472
|
-
const db = body.toString('utf8');
|
|
473
|
-
if (!this.server._safeDbName(db)) {
|
|
474
|
-
this._malicious('COM_INIT_DB with invalid database name');
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
this.server._switchDb(this, db)
|
|
478
|
-
.then(() => this._send(okPacket()))
|
|
479
|
-
.catch(e => this._send(errPacket(e.code || 1049, e.message)));
|
|
480
|
-
break;
|
|
481
|
-
}
|
|
482
|
-
case 0x03: {
|
|
483
|
-
const sql = body.toString('utf8');
|
|
484
|
-
if (sql.length === 0) {
|
|
485
|
-
this._send(errPacket(1065, 'Query was empty'));
|
|
486
|
-
break;
|
|
487
|
-
}
|
|
488
|
-
this._handleQuery(sql);
|
|
489
|
-
break;
|
|
490
|
-
}
|
|
491
|
-
case 0x11: this._handleChangeUser(body, cmdSeq); break; // COM_CHANGE_USER
|
|
492
|
-
case 0x0e: this._send(okPacket()); break; // COM_PING
|
|
493
|
-
case 0x1f: this._send(okPacket()); break; // COM_RESET_CONNECTION
|
|
494
|
-
case 0x16: this._handleStmtPrepare(body); break; // COM_STMT_PREPARE
|
|
495
|
-
case 0x17: this._handleStmtExecute(body); break; // COM_STMT_EXECUTE
|
|
496
|
-
case 0x18: break; // COM_STMT_SEND_LONG_DATA (忽略)
|
|
497
|
-
case 0x19: this._handleStmtClose(body); break; // COM_STMT_CLOSE
|
|
498
|
-
case 0x1a: this._send(okPacket()); break; // COM_STMT_RESET
|
|
499
|
-
case 0x0a: { // COM_PROCESS_INFO
|
|
500
|
-
const seq = this.sequence;
|
|
501
|
-
this.sequence = (this.sequence + 1) & 0xff;
|
|
502
|
-
this._send(encodeLenenc(1));
|
|
503
|
-
this._send(columnDefinition({ name: 'Id', type: MYSQL_TYPE_LONG }));
|
|
504
|
-
this._send(eofPacket());
|
|
505
|
-
this._send(Buffer.from([0x31]));
|
|
506
|
-
this._send(eofPacket());
|
|
507
|
-
break;
|
|
508
|
-
}
|
|
509
|
-
case 0x09: this._send(okPacket(0, 0, SERVER_STATUS_AUTOCOMMIT)); break; // COM_STATISTICS
|
|
510
|
-
default:
|
|
511
|
-
this._send(errPacket(1105, `Unsupported command: ${cmd}`));
|
|
512
|
-
}
|
|
513
|
-
} catch (e) {
|
|
514
|
-
this._send(errPacket(toMysqlErrno(e), e.message));
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
_handleChangeUser(body, cmdSeq) {
|
|
519
|
-
(async () => {
|
|
520
|
-
try {
|
|
521
|
-
let pos = 0;
|
|
522
|
-
const userEnd = body.indexOf(0, pos);
|
|
523
|
-
if (userEnd === -1) throw new Error('malformed COM_CHANGE_USER');
|
|
524
|
-
const user = body.slice(pos, userEnd).toString('utf8');
|
|
525
|
-
pos = userEnd + 1;
|
|
526
|
-
const lenenc = parseLenenc(body, pos);
|
|
527
|
-
let authResponse = Buffer.alloc(0);
|
|
528
|
-
if (lenenc.value !== null) {
|
|
529
|
-
authResponse = body.slice(pos + lenenc.size, pos + lenenc.size + lenenc.value);
|
|
530
|
-
pos += lenenc.size + lenenc.value;
|
|
531
|
-
} else {
|
|
532
|
-
pos += lenenc.size;
|
|
533
|
-
}
|
|
534
|
-
let db = null;
|
|
535
|
-
if (pos < body.length) {
|
|
536
|
-
const dbEnd = body.indexOf(0, pos);
|
|
537
|
-
if (dbEnd !== -1) {
|
|
538
|
-
db = body.slice(pos, dbEnd).toString('utf8');
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
const valid = this.server._checkAuth(user, authResponse, this.seed);
|
|
542
|
-
if (!valid) {
|
|
543
|
-
this._send(errPacket(1045, `Access denied for user '${user}'`));
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
if (db) {
|
|
547
|
-
if (!this.server._canAccessDb(user, db)) {
|
|
548
|
-
this._send(errPacket(1044, `Access denied for user '${user}' to database '${db}'`));
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
try {
|
|
552
|
-
await this.server._getDatabase(db);
|
|
553
|
-
} catch (e) {
|
|
554
|
-
this._send(errPacket(1049, `Unknown database '${db}'`));
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
this.user = user;
|
|
559
|
-
this.currentDb = db;
|
|
560
|
-
this._send(okPacket());
|
|
561
|
-
} catch (e) {
|
|
562
|
-
this._send(errPacket(toMysqlErrno(e), e.message));
|
|
563
|
-
}
|
|
564
|
-
})();
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
async _handleAuth(payload) {
|
|
568
|
-
try {
|
|
569
|
-
const caps = payload.readUInt32LE(0);
|
|
570
|
-
let pos = 32; // 4 caps + 4 maxpacket + 1 charset + 23 reserved
|
|
571
|
-
const userEnd = payload.indexOf(0, pos);
|
|
572
|
-
this.user = payload.slice(pos, userEnd).toString('utf8');
|
|
573
|
-
pos = userEnd + 1;
|
|
574
|
-
let authResponse = Buffer.alloc(0);
|
|
575
|
-
if (caps & CLIENT_SECURE_CONNECTION) {
|
|
576
|
-
const lenenc = parseLenenc(payload, pos);
|
|
577
|
-
if (lenenc.value === null) { pos += lenenc.size; }
|
|
578
|
-
else {
|
|
579
|
-
authResponse = payload.slice(pos + lenenc.size, pos + lenenc.size + lenenc.value);
|
|
580
|
-
pos += lenenc.size + lenenc.value;
|
|
581
|
-
}
|
|
582
|
-
} else {
|
|
583
|
-
const end = payload.indexOf(0, pos);
|
|
584
|
-
if (end !== -1) {
|
|
585
|
-
authResponse = payload.slice(pos, end);
|
|
586
|
-
pos = end + 1;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
let db = null;
|
|
590
|
-
if (caps & CLIENT_CONNECT_WITH_DB) {
|
|
591
|
-
const dbEnd = payload.indexOf(0, pos);
|
|
592
|
-
if (dbEnd !== -1) {
|
|
593
|
-
db = payload.slice(pos, dbEnd).toString('utf8');
|
|
594
|
-
pos = dbEnd + 1;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
this.currentDb = db;
|
|
598
|
-
|
|
599
|
-
const valid = this.server._checkAuth(this.user, authResponse, this.seed);
|
|
600
|
-
if (!valid) {
|
|
601
|
-
this.authFails++;
|
|
602
|
-
this.server._onSecurityEvent({ type: 'auth-fail', user: this.user, fails: this.authFails, remote: this.socket.remoteAddress });
|
|
603
|
-
if (this.authFails >= this.server.maxAuthFails) {
|
|
604
|
-
this.socket.destroy();
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
this._send(errPacket(1045, `Access denied for user '${this.user}'`));
|
|
608
|
-
this.socket.end();
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
this.multiStatements = !!(caps & CLIENT_MULTI_STATEMENTS);
|
|
612
|
-
this.authenticated = true;
|
|
613
|
-
if (this._authTimer) { clearTimeout(this._authTimer); this._authTimer = null; }
|
|
614
|
-
this._armIdleTimer();
|
|
615
|
-
if (db) {
|
|
616
|
-
if (!this.server._canAccessDb(this.user, db)) {
|
|
617
|
-
this._send(errPacket(1044, `Access denied for user '${this.user}' to database '${db}'`));
|
|
618
|
-
this.socket.end();
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
try {
|
|
622
|
-
if (!this.server._dbExists(db)) await this.server.createDatabase(db);
|
|
623
|
-
await this.server._getDatabase(db);
|
|
624
|
-
this.currentDb = db;
|
|
625
|
-
} catch (e) {
|
|
626
|
-
this._send(errPacket(1049, `Unknown database '${db}'`));
|
|
627
|
-
this.socket.end();
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
this._send(okPacket());
|
|
632
|
-
} catch (e) {
|
|
633
|
-
this._send(errPacket(1105, 'auth failed: ' + e.message));
|
|
634
|
-
this.socket.end();
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
countPlaceholders(sql) {
|
|
639
|
-
let n = 0;
|
|
640
|
-
let inStr = null;
|
|
641
|
-
for (let i = 0; i < sql.length; i++) {
|
|
642
|
-
const c = sql[i];
|
|
643
|
-
if (inStr) {
|
|
644
|
-
if (c === '\\' && i + 1 < sql.length) { i++; continue; }
|
|
645
|
-
if (c === inStr) inStr = null;
|
|
646
|
-
continue;
|
|
647
|
-
}
|
|
648
|
-
if (c === "'" || c === '"' || c === '`') { inStr = c; continue; }
|
|
649
|
-
if (c === '?' && sql[i + 1] === '?') { i++; continue; }
|
|
650
|
-
if (c === '?') n++;
|
|
651
|
-
}
|
|
652
|
-
return n;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
_handleStmtPrepare(body) {
|
|
656
|
-
try {
|
|
657
|
-
const sql = body.toString('utf8');
|
|
658
|
-
const numParams = this.countPlaceholders(sql);
|
|
659
|
-
let numColumns = 0;
|
|
660
|
-
try {
|
|
661
|
-
const stmt = parseSQL(sql);
|
|
662
|
-
if (stmt && stmt.type === 'select' && stmt.columns) numColumns = stmt.columns.length;
|
|
663
|
-
} catch (e) { /* 无法解析的 SQL 仍可 prepare */ }
|
|
664
|
-
const stmtId = ++this._stmtSeq;
|
|
665
|
-
this._stmts.set(stmtId, { sql, numParams, numColumns });
|
|
666
|
-
|
|
667
|
-
const pkt = Buffer.alloc(12);
|
|
668
|
-
let off = 0;
|
|
669
|
-
pkt[off++] = 0x00;
|
|
670
|
-
pkt.writeUInt32LE(stmtId, off); off += 4;
|
|
671
|
-
pkt.writeUInt16LE(numColumns, off); off += 2;
|
|
672
|
-
pkt.writeUInt16LE(numParams, off); off += 2;
|
|
673
|
-
pkt[off++] = 0x00;
|
|
674
|
-
pkt.writeUInt16LE(0, off);
|
|
675
|
-
this._send(pkt);
|
|
676
|
-
|
|
677
|
-
if (numParams > 0) {
|
|
678
|
-
for (let i = 0; i < numParams; i++) {
|
|
679
|
-
this._send(columnDefinition({ name: '?', type: MYSQL_TYPE_VAR_STRING }));
|
|
680
|
-
}
|
|
681
|
-
this._send(eofPacket());
|
|
682
|
-
}
|
|
683
|
-
if (numColumns > 0) {
|
|
684
|
-
for (let i = 0; i < numColumns; i++) {
|
|
685
|
-
this._send(columnDefinition({ name: 'col' + (i + 1), type: MYSQL_TYPE_VAR_STRING }));
|
|
686
|
-
}
|
|
687
|
-
this._send(eofPacket());
|
|
688
|
-
}
|
|
689
|
-
} catch (e) {
|
|
690
|
-
this._send(errPacket(toMysqlErrno(e), e.message));
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
_handleStmtClose(body) {
|
|
695
|
-
if (body.length >= 4) {
|
|
696
|
-
const stmtId = body.readUInt32LE(0);
|
|
697
|
-
this._stmts.delete(stmtId);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
_handleStmtExecute(body) {
|
|
702
|
-
try {
|
|
703
|
-
if (body.length < 9) throw new Error('malformed COM_STMT_EXECUTE');
|
|
704
|
-
const stmtId = body.readUInt32LE(0);
|
|
705
|
-
const stmt = this._stmts.get(stmtId);
|
|
706
|
-
if (!stmt) {
|
|
707
|
-
this._send(errPacket(1243, 'Unknown prepared statement handler (' + stmtId + ') given to mysqld_stmt_execute'));
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
const flags = body[4];
|
|
711
|
-
const iteration = body.readUInt32LE(5);
|
|
712
|
-
let values = null;
|
|
713
|
-
if (stmt.numParams > 0) {
|
|
714
|
-
let off = 9;
|
|
715
|
-
const numParams = stmt.numParams;
|
|
716
|
-
const nullBitmapLen = Math.ceil(numParams / 8);
|
|
717
|
-
if (body.length < off + nullBitmapLen) throw new Error('malformed COM_STMT_EXECUTE params');
|
|
718
|
-
const nullBitmap = body.slice(off, off + nullBitmapLen);
|
|
719
|
-
off += nullBitmapLen;
|
|
720
|
-
if (off >= body.length) throw new Error('malformed COM_STMT_EXECUTE params');
|
|
721
|
-
const newParamsBound = body[off];
|
|
722
|
-
off += 1;
|
|
723
|
-
let types = stmt.types;
|
|
724
|
-
if (newParamsBound & 0x01) {
|
|
725
|
-
if (body.length < off + numParams * 2) throw new Error('malformed COM_STMT_EXECUTE types');
|
|
726
|
-
types = [];
|
|
727
|
-
for (let i = 0; i < numParams; i++) types.push(body.readUInt16LE(off + i * 2));
|
|
728
|
-
stmt.types = types;
|
|
729
|
-
off += numParams * 2;
|
|
730
|
-
}
|
|
731
|
-
if (!types) throw new Error('Parameter types unknown for prepared statement');
|
|
732
|
-
values = [];
|
|
733
|
-
for (let i = 0; i < numParams; i++) {
|
|
734
|
-
const isNull = nullBitmap[Math.floor(i / 8)] & (1 << (i % 8));
|
|
735
|
-
if (isNull) { values.push(null); continue; }
|
|
736
|
-
const type = types[i] & 0xff;
|
|
737
|
-
const parsed = this._readParamValue(body, off, type);
|
|
738
|
-
values.push(parsed.value);
|
|
739
|
-
off = parsed.off;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
this._handleQuery(stmt.sql, values !== null && values.length > 0 ? values : undefined);
|
|
743
|
-
} catch (e) {
|
|
744
|
-
this._send(errPacket(toMysqlErrno(e), e.message));
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
_readParamValue(buf, off, type) {
|
|
749
|
-
switch (type) {
|
|
750
|
-
case MYSQL_TYPE_NULL: return { value: null, off };
|
|
751
|
-
case 0x01: { const v = buf.readInt8(off); return { value: v, off: off + 1 }; } // TINY
|
|
752
|
-
case 0x02: { const v = buf.readInt16LE(off); return { value: v, off: off + 2 }; } // SHORT
|
|
753
|
-
case 0x03: { const v = buf.readInt32LE(off); return { value: v, off: off + 4 }; } // LONG
|
|
754
|
-
case 0x08: { const v = buf.readBigInt64LE(off); return { value: Number(v), off: off + 8 }; } // LONGLONG
|
|
755
|
-
case 0x04: { const v = buf.readFloatLE(off); return { value: v, off: off + 4 }; } // FLOAT
|
|
756
|
-
case MYSQL_TYPE_DOUBLE: { const v = buf.readDoubleLE(off); return { value: v, off: off + 8 }; }
|
|
757
|
-
case 0x0a: case 0x07: case 0x0b: case MYSQL_TYPE_DATETIME: { // DATE/TIMESTAMP/DATETIME/TIME
|
|
758
|
-
const len = buf[off];
|
|
759
|
-
off += 1;
|
|
760
|
-
if (len === 0) return { value: null, off };
|
|
761
|
-
let value;
|
|
762
|
-
if (type === 0x0b) { // TIME
|
|
763
|
-
let sign = 1;
|
|
764
|
-
let p = off;
|
|
765
|
-
if (buf[p] !== 0) sign = -1;
|
|
766
|
-
p += 1;
|
|
767
|
-
const days = buf.readUInt32LE(p); p += 4;
|
|
768
|
-
const hour = buf[p++];
|
|
769
|
-
const min = buf[p++];
|
|
770
|
-
const sec = buf[p++];
|
|
771
|
-
value = sign * (days * 24 + hour) + ':' + String(min).padStart(2, '0') + ':' + String(sec).padStart(2, '0');
|
|
772
|
-
} else {
|
|
773
|
-
let p = off;
|
|
774
|
-
const year = buf.readUInt16LE(p); p += 2;
|
|
775
|
-
const month = buf[p++];
|
|
776
|
-
const day = buf[p++];
|
|
777
|
-
let hour = 0, minute = 0, second = 0;
|
|
778
|
-
if (len >= 7) { hour = buf[p++]; minute = buf[p++]; second = buf[p++]; }
|
|
779
|
-
value = String(year).padStart(4, '0') + '-' + String(month).padStart(2, '0') + '-' + String(day).padStart(2, '0');
|
|
780
|
-
if (len >= 7) value += ' ' + String(hour).padStart(2, '0') + ':' + String(minute).padStart(2, '0') + ':' + String(second).padStart(2, '0');
|
|
781
|
-
}
|
|
782
|
-
off += len;
|
|
783
|
-
return { value, off };
|
|
784
|
-
}
|
|
785
|
-
case 0xfc: case 0xfd: case 0xfe: case 0xf9: case 0xf6: { // BLOB / VAR_STRING / STRING / ...
|
|
786
|
-
const { value, off: noff } = readLenenc(buf, off);
|
|
787
|
-
return { value, off: noff };
|
|
788
|
-
}
|
|
789
|
-
case 0xf0: { // NULL
|
|
790
|
-
return { value: null, off };
|
|
791
|
-
}
|
|
792
|
-
default: {
|
|
793
|
-
const { value, off: noff } = readLenenc(buf, off);
|
|
794
|
-
return { value, off: noff };
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
_handleQuery(sql, values) {
|
|
800
|
-
(async () => {
|
|
801
|
-
try {
|
|
802
|
-
const hasParams = values !== undefined && values !== null && values.length > 0;
|
|
803
|
-
const processedSql = hasParams ? applyParams(sql, values) : sql;
|
|
804
|
-
const statements = splitStatements(processedSql);
|
|
805
|
-
if (statements.length > 1 && !this.multiStatements) {
|
|
806
|
-
throw new Error(`too many statements (${statements.length} > 1)`);
|
|
807
|
-
}
|
|
808
|
-
for (const stmtSql of statements) {
|
|
809
|
-
const stmt = parseSQL(stmtSql);
|
|
810
|
-
switch (stmt.type) {
|
|
811
|
-
case 'use': {
|
|
812
|
-
if (!this.server._canAccessDb(this.user, stmt.database)) {
|
|
813
|
-
this._send(errPacket(1044, `Access denied for user '${this.user}' to database '${stmt.database}'`));
|
|
814
|
-
continue;
|
|
815
|
-
}
|
|
816
|
-
try {
|
|
817
|
-
await this.server._getDatabase(stmt.database);
|
|
818
|
-
this.currentDb = stmt.database;
|
|
819
|
-
this._send(okPacket());
|
|
820
|
-
} catch (e) {
|
|
821
|
-
this._send(errPacket(1049, `Unknown database '${stmt.database}'`));
|
|
822
|
-
}
|
|
823
|
-
continue;
|
|
824
|
-
}
|
|
825
|
-
case 'showDatabases': {
|
|
826
|
-
const dbs = await this.server.listDatabases();
|
|
827
|
-
const allowed = this.server._userDbList(this.user);
|
|
828
|
-
const names = allowed === null ? dbs : dbs.filter(n => allowed.includes(n));
|
|
829
|
-
const { packets, sequence } = resultSetPacket({
|
|
830
|
-
type: 'showDatabases', columns: ['Database'], rows: names.map(n => [n]),
|
|
831
|
-
}, null, this.sequence);
|
|
832
|
-
this.sequence = sequence;
|
|
833
|
-
this.socket.write(Buffer.concat(packets));
|
|
834
|
-
continue;
|
|
835
|
-
}
|
|
836
|
-
case 'createDatabase': {
|
|
837
|
-
if (!this.server._canAccessDb(this.user, stmt.database)) {
|
|
838
|
-
this._send(errPacket(1044, `Access denied for user '${this.user}' to database '${stmt.database}'`));
|
|
839
|
-
continue;
|
|
840
|
-
}
|
|
841
|
-
await this.server.createDatabase(stmt.database, { ifNotExists: stmt.ifNotExists });
|
|
842
|
-
this._send(okPacket());
|
|
843
|
-
continue;
|
|
844
|
-
}
|
|
845
|
-
case 'dropDatabase': {
|
|
846
|
-
if (!this.server._canAccessDb(this.user, stmt.database)) {
|
|
847
|
-
this._send(errPacket(1044, `Access denied for user '${this.user}' to database '${stmt.database}'`));
|
|
848
|
-
continue;
|
|
849
|
-
}
|
|
850
|
-
await this.server.dropDatabase(stmt.database, { ifExists: stmt.ifExists });
|
|
851
|
-
this._send(okPacket());
|
|
852
|
-
continue;
|
|
853
|
-
}
|
|
854
|
-
default:
|
|
855
|
-
break;
|
|
856
|
-
}
|
|
857
|
-
const routed = await this.server._route(stmt, stmtSql, this.currentDb, this.user);
|
|
858
|
-
const engine = routed.engine;
|
|
859
|
-
const sqlToRun = routed.sql;
|
|
860
|
-
const r = await executeSQL(engine, sqlToRun, {
|
|
861
|
-
allowComments: this.server.allowComments,
|
|
862
|
-
safety: this.server.safety,
|
|
863
|
-
maxStatements: 1,
|
|
864
|
-
session: this.session,
|
|
865
|
-
});
|
|
866
|
-
if (r.type === 'select' || r.type === 'showTables' || r.type === 'showDatabases' || r.type === 'describe'
|
|
867
|
-
|| r.type === 'showColumns' || r.type === 'showIndex' || r.type === 'showCreateTable'
|
|
868
|
-
|| r.type === 'showVariables' || r.type === 'showStatus' || r.type === 'showGrants' || r.type === 'showWarnings') {
|
|
869
|
-
this.session.foundRows = (r.raw && r.raw.length !== undefined) ? r.raw.length : (r.rows ? r.rows.length : 0);
|
|
870
|
-
let schema = null;
|
|
871
|
-
if (r.table) {
|
|
872
|
-
schema = engine.getTableSchema
|
|
873
|
-
? await engine.getTableSchema(r.table)
|
|
874
|
-
: (engine._schemas ? engine._schemas[r.table] : null);
|
|
875
|
-
}
|
|
876
|
-
const { packets, sequence } = hasParams
|
|
877
|
-
? binaryResultSetPacket(r, schema, this.sequence)
|
|
878
|
-
: resultSetPacket(r, schema, this.sequence);
|
|
879
|
-
this.sequence = sequence;
|
|
880
|
-
this.socket.write(Buffer.concat(packets));
|
|
881
|
-
} else {
|
|
882
|
-
if (r.type === 'insert') {
|
|
883
|
-
this.session.lastInsertId = r.insertId;
|
|
884
|
-
this.session.rowCount = r.affectedRows;
|
|
885
|
-
} else if (r.type === 'update' || r.type === 'delete' || r.type === 'truncate') {
|
|
886
|
-
this.session.rowCount = r.affectedRows;
|
|
887
|
-
}
|
|
888
|
-
this._send(okPacket(r.affectedRows || 0, r.insertId || 0));
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
} catch (e) {
|
|
892
|
-
this._send(errPacket(toMysqlErrno(e), e.message));
|
|
893
|
-
}
|
|
894
|
-
})();
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
class MysqlServer {
|
|
899
|
-
constructor(options = {}) {
|
|
900
|
-
this.options = options;
|
|
901
|
-
this.port = options.port ?? 3306;
|
|
902
|
-
this.host = options.host || '127.0.0.1';
|
|
903
|
-
this.user = options.user || null;
|
|
904
|
-
this.password = options.password || null;
|
|
905
|
-
this.auth = options.auth || null;
|
|
906
|
-
this.safety = options.safety !== false;
|
|
907
|
-
this.allowComments = options.allowComments === true;
|
|
908
|
-
this.maxPacketSize = options.maxPacketSize || 1024 * 1024;
|
|
909
|
-
this.handshakeTimeout = options.handshakeTimeout != null ? options.handshakeTimeout : 10000;
|
|
910
|
-
this.idleTimeout = options.idleTimeout != null ? options.idleTimeout : 300000;
|
|
911
|
-
this.maxAuthFails = options.maxAuthFails || 3;
|
|
912
|
-
this.maxConnections = options.maxConnections || 128;
|
|
913
|
-
this._engine = null;
|
|
914
|
-
this._ownEngine = false;
|
|
915
|
-
this._databases = new Map();
|
|
916
|
-
this._dbDir = options.dataDir && options.dataDir !== ':memory:' ? path.resolve(options.dataDir) : null;
|
|
917
|
-
this._defaultDbName = options.defaultDatabase || 'default';
|
|
918
|
-
this._connectionCounter = 0;
|
|
919
|
-
this._sockets = new Set();
|
|
920
|
-
this._securityHandler = typeof options.onSecurityEvent === 'function' ? options.onSecurityEvent : null;
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
_onSecurityEvent(event) {
|
|
924
|
-
if (this._securityHandler) {
|
|
925
|
-
try { this._securityHandler(event); } catch (e) {}
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
async _getEngine(dbName) {
|
|
930
|
-
if (dbName) return this._getDatabase(dbName);
|
|
931
|
-
if (this._dbDir) {
|
|
932
|
-
return this._getDatabase(this._defaultDbName, { autoCreate: true });
|
|
933
|
-
}
|
|
934
|
-
if (this._engine) return this._engine;
|
|
935
|
-
if (this.options.engine || this.options.database) {
|
|
936
|
-
this._engine = this.options.engine || this.options.database;
|
|
937
|
-
if (typeof this._engine.start === 'function') await this._engine.start();
|
|
938
|
-
} else {
|
|
939
|
-
this._engine = new Database(this.options.filename || ':memory:');
|
|
940
|
-
if (typeof this._engine.start === 'function') await this._engine.start();
|
|
941
|
-
this._ownEngine = true;
|
|
942
|
-
}
|
|
943
|
-
return this._engine;
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
_dbExists(name) {
|
|
947
|
-
if (this._databases.has(name)) return true;
|
|
948
|
-
const dir = this._dbPath(name);
|
|
949
|
-
return !!(dir && fs.existsSync(dir));
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
// 语句路由:解析库前缀(db.table)与 SHOW TABLES FROM db,
|
|
953
|
-
// 决定执行引擎,并把 db.table 改写为 table 后交给对应库引擎执行。
|
|
954
|
-
async _route(stmt, sql, currentDb, user) {
|
|
955
|
-
if (stmt.type === 'showTables' && stmt.database) {
|
|
956
|
-
if (user && this.auth && !this._canAccessDb(user, stmt.database)) {
|
|
957
|
-
const err = new Error(`Access denied for user '${user}' to database '${stmt.database}'`);
|
|
958
|
-
err.code = 1044;
|
|
959
|
-
throw err;
|
|
960
|
-
}
|
|
961
|
-
return { engine: await this._getDatabase(stmt.database), sql };
|
|
962
|
-
}
|
|
963
|
-
const tables = [];
|
|
964
|
-
const collect = (s) => {
|
|
965
|
-
if (!s) return;
|
|
966
|
-
if (s.type === 'createTable') tables.push(s.name);
|
|
967
|
-
else if (s.type === 'dropTable') tables.push(s.table);
|
|
968
|
-
else if (s.type === 'insert') tables.push(s.name);
|
|
969
|
-
else if (s.type === 'update') tables.push(s.table);
|
|
970
|
-
else if (s.type === 'delete') tables.push(s.table);
|
|
971
|
-
else if (s.type === 'describe') tables.push(s.table);
|
|
972
|
-
else if (s.type === 'select' && s.from) {
|
|
973
|
-
for (const t of s.from.tables) tables.push(t.table);
|
|
974
|
-
for (const j of s.from.joins) tables.push(j.item.table);
|
|
975
|
-
}
|
|
976
|
-
};
|
|
977
|
-
collect(stmt);
|
|
978
|
-
const isInfoSchema = tables.some(t => t && String(t).toLowerCase().startsWith('information_schema.'));
|
|
979
|
-
if (isInfoSchema) {
|
|
980
|
-
return { engine: await this._getEngine(currentDb), sql };
|
|
981
|
-
}
|
|
982
|
-
let db = null;
|
|
983
|
-
for (const t of tables) {
|
|
984
|
-
if (t && t.indexOf('.') !== -1) {
|
|
985
|
-
const d = t.split('.')[0];
|
|
986
|
-
if (db && d !== db) {
|
|
987
|
-
const err = new Error(`Cross-database references are not supported in one statement`);
|
|
988
|
-
err.code = 1105;
|
|
989
|
-
throw err;
|
|
990
|
-
}
|
|
991
|
-
db = d;
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
if (db) {
|
|
995
|
-
if (user && this.auth && !this._canAccessDb(user, db)) {
|
|
996
|
-
const err = new Error(`Access denied for user '${user}' to database '${db}'`);
|
|
997
|
-
err.code = 1044;
|
|
998
|
-
throw err;
|
|
999
|
-
}
|
|
1000
|
-
if (!this._dbExists(db)) await this.createDatabase(db);
|
|
1001
|
-
const engine = await this._getDatabase(db);
|
|
1002
|
-
let s = sql;
|
|
1003
|
-
for (const t of tables) {
|
|
1004
|
-
if (t && t.indexOf('.') !== -1) {
|
|
1005
|
-
s = s.replace(new RegExp('\\b' + t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'g'), t.split('.')[1]);
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
return { engine, sql: s };
|
|
1009
|
-
}
|
|
1010
|
-
if (this._dbDir && !currentDb && this.auth && !this._canAccessDb(user, this._defaultDbName)) {
|
|
1011
|
-
const err = new Error('No database selected');
|
|
1012
|
-
err.code = 1046;
|
|
1013
|
-
throw err;
|
|
1014
|
-
}
|
|
1015
|
-
return { engine: await this._getEngine(currentDb), sql };
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
_safeDbName(name) {
|
|
1019
|
-
if (typeof name !== 'string' || name.length === 0 || name.length > 64) return false;
|
|
1020
|
-
if (!/^[a-zA-Z0-9_$.\-]+$/.test(name)) return false;
|
|
1021
|
-
if (name === '.' || name === '..' || name.includes('..')) return false;
|
|
1022
|
-
return true;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
_dbPath(name) {
|
|
1026
|
-
if (!this._dbDir) return null;
|
|
1027
|
-
if (!this._safeDbName(name)) return null;
|
|
1028
|
-
return path.join(this._dbDir, name);
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
async _getDatabase(name, opts = {}) {
|
|
1032
|
-
if (!this._safeDbName(name)) {
|
|
1033
|
-
const err = new Error(`Unknown database '${name}'`);
|
|
1034
|
-
err.code = 1049;
|
|
1035
|
-
throw err;
|
|
1036
|
-
}
|
|
1037
|
-
if (this._databases.has(name)) return this._databases.get(name);
|
|
1038
|
-
const dir = this._dbPath(name);
|
|
1039
|
-
if (dir && !fs.existsSync(dir)) {
|
|
1040
|
-
if (opts.autoCreate) {
|
|
1041
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1042
|
-
} else {
|
|
1043
|
-
const err = new Error(`Unknown database '${name}'`);
|
|
1044
|
-
err.code = 1049;
|
|
1045
|
-
throw err;
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
const engine = dir ? new Database(dir, { mode: 'hybrid' }) : new Database(':memory:');
|
|
1049
|
-
if (typeof engine.start === 'function') await engine.start();
|
|
1050
|
-
this._databases.set(name, engine);
|
|
1051
|
-
return engine;
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
async listDatabases() {
|
|
1055
|
-
if (!this._dbDir) return ['jsql'];
|
|
1056
|
-
let names = [];
|
|
1057
|
-
if (fs.existsSync(this._dbDir)) {
|
|
1058
|
-
names = fs.readdirSync(this._dbDir).filter(n => {
|
|
1059
|
-
const p = path.join(this._dbDir, n);
|
|
1060
|
-
return fs.statSync(p).isDirectory() && this._safeDbName(n);
|
|
1061
|
-
});
|
|
1062
|
-
}
|
|
1063
|
-
for (const n of this._databases.keys()) {
|
|
1064
|
-
if (!names.includes(n)) names.push(n);
|
|
1065
|
-
}
|
|
1066
|
-
return names.sort();
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
async createDatabase(name, opts = {}) {
|
|
1070
|
-
if (!this._safeDbName(name)) {
|
|
1071
|
-
const err = new Error(`Invalid database name '${name}'`);
|
|
1072
|
-
err.code = 1105;
|
|
1073
|
-
throw err;
|
|
1074
|
-
}
|
|
1075
|
-
if (this._databases.has(name)) {
|
|
1076
|
-
if (opts.ifNotExists) return this._databases.get(name);
|
|
1077
|
-
const err = new Error(`Can't create database '${name}'; database exists`);
|
|
1078
|
-
err.code = 1007;
|
|
1079
|
-
throw err;
|
|
1080
|
-
}
|
|
1081
|
-
const dir = this._dbPath(name);
|
|
1082
|
-
if (dir && fs.existsSync(dir)) {
|
|
1083
|
-
if (opts.ifNotExists) return this._getDatabase(name);
|
|
1084
|
-
const err = new Error(`Can't create database '${name}'; database exists`);
|
|
1085
|
-
err.code = 1007;
|
|
1086
|
-
throw err;
|
|
1087
|
-
}
|
|
1088
|
-
if (dir) fs.mkdirSync(dir, { recursive: true });
|
|
1089
|
-
const engine = dir ? new Database(dir, { mode: 'hybrid' }) : new Database(':memory:');
|
|
1090
|
-
if (typeof engine.start === 'function') await engine.start();
|
|
1091
|
-
this._databases.set(name, engine);
|
|
1092
|
-
return engine;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
async dropDatabase(name, opts = {}) {
|
|
1096
|
-
if (!this._safeDbName(name)) {
|
|
1097
|
-
const err = new Error(`Unknown database '${name}'`);
|
|
1098
|
-
err.code = 1049;
|
|
1099
|
-
throw err;
|
|
1100
|
-
}
|
|
1101
|
-
const engine = this._databases.get(name);
|
|
1102
|
-
if (!engine) {
|
|
1103
|
-
if (opts.ifExists) return;
|
|
1104
|
-
const err = new Error(`Can't drop database '${name}'; database doesn't exist`);
|
|
1105
|
-
err.code = 1008;
|
|
1106
|
-
throw err;
|
|
1107
|
-
}
|
|
1108
|
-
if (typeof engine.stop === 'function') {
|
|
1109
|
-
try { await engine.stop(); } catch (e) {}
|
|
1110
|
-
}
|
|
1111
|
-
this._databases.delete(name);
|
|
1112
|
-
const dir = this._dbPath(name);
|
|
1113
|
-
if (dir && fs.existsSync(dir)) {
|
|
1114
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
_userDbList(user) {
|
|
1119
|
-
if (!this.auth) return null;
|
|
1120
|
-
const entry = this.auth[user];
|
|
1121
|
-
if (entry && typeof entry === 'object') {
|
|
1122
|
-
if (Array.isArray(entry.databases)) return entry.databases;
|
|
1123
|
-
return null;
|
|
1124
|
-
}
|
|
1125
|
-
return [];
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
_canAccessDb(user, dbName) {
|
|
1129
|
-
const list = this._userDbList(user);
|
|
1130
|
-
if (list === null) return true;
|
|
1131
|
-
return list.includes(dbName);
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
async _switchDb(conn, dbName) {
|
|
1135
|
-
if (!this._canAccessDb(conn.user, dbName)) {
|
|
1136
|
-
const err = new Error(`Access denied for user '${conn.user}' to database '${dbName}'`);
|
|
1137
|
-
err.code = 1044;
|
|
1138
|
-
throw err;
|
|
1139
|
-
}
|
|
1140
|
-
await this._getDatabase(dbName);
|
|
1141
|
-
conn.currentDb = dbName;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
_checkAuth(user, authResponse, seed) {
|
|
1145
|
-
if (this.auth) {
|
|
1146
|
-
if (!Object.prototype.hasOwnProperty.call(this.auth, user)) return false;
|
|
1147
|
-
const entry = this.auth[user];
|
|
1148
|
-
const pwd = entry && typeof entry === 'object' ? entry.password : entry;
|
|
1149
|
-
if (!pwd) return authResponse.length === 0;
|
|
1150
|
-
const pwdHash1 = crypto.createHash('sha1').update(pwd).digest();
|
|
1151
|
-
const pwdHash2 = crypto.createHash('sha1').update(pwdHash1).digest();
|
|
1152
|
-
const seedHash = crypto.createHash('sha1').update(Buffer.concat([seed, pwdHash2])).digest();
|
|
1153
|
-
const expected = Buffer.alloc(20);
|
|
1154
|
-
for (let i = 0; i < 20; i++) expected[i] = pwdHash1[i] ^ seedHash[i];
|
|
1155
|
-
return authResponse.length === 20 && crypto.timingSafeEqual(expected, authResponse);
|
|
1156
|
-
}
|
|
1157
|
-
if (this.user === null) {
|
|
1158
|
-
// 未配置认证:默认拒绝所有登录(防配置遗漏导致认证旁路)。
|
|
1159
|
-
// 仅当显式设置 noAuth: true 时允许无认证连接(仅限本地开发)。
|
|
1160
|
-
return this.options.noAuth === true;
|
|
1161
|
-
}
|
|
1162
|
-
if (user !== this.user) return false;
|
|
1163
|
-
if (!this.password) return authResponse.length === 0;
|
|
1164
|
-
const pwdHash1 = crypto.createHash('sha1').update(this.password).digest();
|
|
1165
|
-
const pwdHash2 = crypto.createHash('sha1').update(pwdHash1).digest();
|
|
1166
|
-
const seedHash = crypto.createHash('sha1').update(Buffer.concat([seed, pwdHash2])).digest();
|
|
1167
|
-
const expected = Buffer.alloc(20);
|
|
1168
|
-
for (let i = 0; i < 20; i++) expected[i] = pwdHash1[i] ^ seedHash[i];
|
|
1169
|
-
return authResponse.length === 20 && crypto.timingSafeEqual(expected, authResponse);
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
listen(cb) {
|
|
1173
|
-
const ready = this._dbDir ? Promise.resolve() : this._getEngine();
|
|
1174
|
-
ready.then(() => {
|
|
1175
|
-
this._server = net.createServer(socket => {
|
|
1176
|
-
if (this._sockets.size >= this.maxConnections) {
|
|
1177
|
-
this._onSecurityEvent({ type: 'max-connections', remote: socket.remoteAddress });
|
|
1178
|
-
socket.destroy();
|
|
1179
|
-
return;
|
|
1180
|
-
}
|
|
1181
|
-
this._sockets.add(socket);
|
|
1182
|
-
socket.on('close', () => this._sockets.delete(socket));
|
|
1183
|
-
new MysqlConnection(socket, this);
|
|
1184
|
-
});
|
|
1185
|
-
this._server.once('error', err => {
|
|
1186
|
-
if (err && err.code === 'EADDRINUSE') {
|
|
1187
|
-
const friendly = new Error(
|
|
1188
|
-
`MysqlServer 端口 ${this.port} 已被占用(EADDRINUSE)。` +
|
|
1189
|
-
`请换一个空闲端口,或传 port: 0 让系统分配临时端口。`
|
|
1190
|
-
);
|
|
1191
|
-
friendly.code = 'EADDRINUSE';
|
|
1192
|
-
friendly.cause = err;
|
|
1193
|
-
if (cb) cb(friendly);
|
|
1194
|
-
else throw friendly;
|
|
1195
|
-
} else if (cb) cb(err);
|
|
1196
|
-
});
|
|
1197
|
-
this._server.listen(this.port, this.host, cb || (() => {}));
|
|
1198
|
-
}).catch(err => {
|
|
1199
|
-
if (cb) cb(err);
|
|
1200
|
-
else throw err;
|
|
1201
|
-
});
|
|
1202
|
-
return this;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
get address() {
|
|
1206
|
-
return this._server ? this._server.address() : null;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
close(cb) {
|
|
1210
|
-
const done = () => {
|
|
1211
|
-
for (const s of this._sockets) s.destroy();
|
|
1212
|
-
const stops = [];
|
|
1213
|
-
if (this._ownEngine && this._engine && typeof this._engine.stop === 'function') {
|
|
1214
|
-
stops.push(this._engine.stop());
|
|
1215
|
-
}
|
|
1216
|
-
for (const engine of this._databases.values()) {
|
|
1217
|
-
if (engine && typeof engine.stop === 'function') stops.push(engine.stop());
|
|
1218
|
-
}
|
|
1219
|
-
this._databases.clear();
|
|
1220
|
-
if (stops.length > 0) {
|
|
1221
|
-
Promise.allSettled(stops).then(() => cb && cb());
|
|
1222
|
-
} else if (cb) cb();
|
|
1223
|
-
};
|
|
1224
|
-
if (this._server) {
|
|
1225
|
-
this._server.close(() => done());
|
|
1226
|
-
} else {
|
|
1227
|
-
done();
|
|
1228
|
-
}
|
|
1229
|
-
return this;
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
function createMysqlServer(options) {
|
|
1234
|
-
return new MysqlServer(options || {});
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
module.exports = { createMysqlServer, MysqlServer, MysqlConnection };
|
|
1
|
+
// © Vexify 2026 All Rights Reserved.
|
|
2
|
+
/**
|
|
3
|
+
* MySQL 协议服务端 — TCP 3306
|
|
4
|
+
* 实现 MySQL 握手/认证/命令循环,任何 mysql/mysql2 客户端可直接连接。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const net = require('net');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const { executeSQL, parseSQL, splitStatements, applyParams } = require('./sql');
|
|
12
|
+
const Database = require('./database');
|
|
13
|
+
|
|
14
|
+
const SERVER_VERSION = '8.0.0-jsql-neo';
|
|
15
|
+
const CLIENT_PROTOCOL_41 = 0x00000001 << 9;
|
|
16
|
+
const CLIENT_SECURE_CONNECTION = 0x00008000;
|
|
17
|
+
const CLIENT_PLUGIN_AUTH = 0x00080000;
|
|
18
|
+
const CLIENT_CONNECT_WITH_DB = 0x00000008;
|
|
19
|
+
const CLIENT_LONG_PASSWORD = 0x00000001;
|
|
20
|
+
const CLIENT_TRANSACTIONS = 0x00002000;
|
|
21
|
+
const CLIENT_MULTI_STATEMENTS = 0x00010000;
|
|
22
|
+
const CLIENT_MULTI_RESULTS = 0x00020000;
|
|
23
|
+
const CLIENT_LONG_FLAG = 0x00000004;
|
|
24
|
+
const CLIENT_DEPRECATE_EOF = 0x01000000;
|
|
25
|
+
const SERVER_STATUS_AUTOCOMMIT = 0x0002;
|
|
26
|
+
const CHARSET_UTF8 = 0x21;
|
|
27
|
+
|
|
28
|
+
const MYSQL_TYPE_TINY = 1, MYSQL_TYPE_LONG = 3, MYSQL_TYPE_LONGLONG = 8,
|
|
29
|
+
MYSQL_TYPE_DATE = 10, MYSQL_TYPE_DATETIME = 12, MYSQL_TYPE_DOUBLE = 5,
|
|
30
|
+
MYSQL_TYPE_STRING = 254, MYSQL_TYPE_VAR_STRING = 253, MYSQL_TYPE_BLOB = 252,
|
|
31
|
+
MYSQL_TYPE_JSON = 245, MYSQL_TYPE_NULL = 6;
|
|
32
|
+
|
|
33
|
+
function encodeLenenc(value) {
|
|
34
|
+
if (typeof value === 'bigint') value = Number(value);
|
|
35
|
+
if (value === null) return Buffer.from([0xfb]);
|
|
36
|
+
if (value < 0xfb) return Buffer.from([value]);
|
|
37
|
+
if (value <= 0xffff) {
|
|
38
|
+
const b = Buffer.alloc(3);
|
|
39
|
+
b[0] = 0xfc; b.writeUInt16LE(value, 1);
|
|
40
|
+
return b;
|
|
41
|
+
}
|
|
42
|
+
if (value <= 0xffffff) {
|
|
43
|
+
const b = Buffer.alloc(4);
|
|
44
|
+
b[0] = 0xfd; b.writeUIntLE(value, 1, 3);
|
|
45
|
+
return b;
|
|
46
|
+
}
|
|
47
|
+
const b = Buffer.alloc(9);
|
|
48
|
+
b[0] = 0xfe; b.writeUInt32LE(value, 1); b.writeUInt32LE(Math.floor(value / 4294967296), 5);
|
|
49
|
+
return b;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function encodeLenencString(str) {
|
|
53
|
+
const buf = Buffer.from(String(str), 'utf8');
|
|
54
|
+
return Buffer.concat([encodeLenenc(buf.length), buf]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseLenenc(buf, offset) {
|
|
58
|
+
const first = buf[offset];
|
|
59
|
+
if (first < 0xfb) return { value: first, size: 1 };
|
|
60
|
+
if (first === 0xfb) return { value: null, size: 1 };
|
|
61
|
+
if (first === 0xfc) return { value: buf.readUInt16LE(offset + 1), size: 3 };
|
|
62
|
+
if (first === 0xfd) return { value: buf.readUIntLE(offset + 1, 3), size: 4 };
|
|
63
|
+
return { value: buf.readUInt32LE(offset + 1), size: 5 };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readLenenc(buf, offset) {
|
|
67
|
+
const { value, size } = parseLenenc(buf, offset);
|
|
68
|
+
if (value === null) return { value: null, off: offset + size };
|
|
69
|
+
const str = buf.slice(offset + size, offset + size + value).toString('utf8');
|
|
70
|
+
return { value: str, off: offset + size + value };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class PacketBuilder {
|
|
74
|
+
constructor() {
|
|
75
|
+
this.bufs = [];
|
|
76
|
+
}
|
|
77
|
+
byte(v) { this.bufs.push(Buffer.from([v & 0xff])); return this; }
|
|
78
|
+
int16(v) { const b = Buffer.alloc(2); b.writeUInt16LE(v & 0xffff); this.bufs.push(b); return this; }
|
|
79
|
+
int32(v) { const b = Buffer.alloc(4); b.writeUInt32LE(v >>> 0); this.bufs.push(b); return this; }
|
|
80
|
+
raw(buf) { this.bufs.push(buf); return this; }
|
|
81
|
+
string(str) { this.bufs.push(Buffer.from(str, 'utf8')); return this; }
|
|
82
|
+
nul(str) { this.bufs.push(Buffer.concat([Buffer.from(String(str), 'utf8'), Buffer.from([0])])); return this; }
|
|
83
|
+
bytes(list) { this.bufs.push(Buffer.from(list)); return this; }
|
|
84
|
+
build() { return Buffer.concat(this.bufs); }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function handshakePacket(connectionId, seed) {
|
|
88
|
+
const b = new PacketBuilder();
|
|
89
|
+
b.byte(10);
|
|
90
|
+
b.nul(SERVER_VERSION);
|
|
91
|
+
b.int32(connectionId);
|
|
92
|
+
b.bytes(seed.slice(0, 8));
|
|
93
|
+
b.byte(0);
|
|
94
|
+
const caps = CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION | CLIENT_PLUGIN_AUTH | CLIENT_CONNECT_WITH_DB | CLIENT_LONG_PASSWORD | CLIENT_TRANSACTIONS | CLIENT_MULTI_STATEMENTS | CLIENT_MULTI_RESULTS | CLIENT_LONG_FLAG;
|
|
95
|
+
b.int16(caps & 0xffff);
|
|
96
|
+
b.byte(CHARSET_UTF8);
|
|
97
|
+
b.int16(SERVER_STATUS_AUTOCOMMIT);
|
|
98
|
+
b.int16((caps >>> 16) & 0xffff);
|
|
99
|
+
b.byte(21);
|
|
100
|
+
b.bytes([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
|
101
|
+
b.bytes(seed.slice(8, 20));
|
|
102
|
+
b.byte(0);
|
|
103
|
+
b.nul('mysql_native_password');
|
|
104
|
+
return b.build();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 生成握手 seed:使用可打印 ASCII(33-126),避开 0x00。
|
|
109
|
+
* 老客户端按 C 字符串读取 seed,若含 \0 会提前截断导致握手失败/断连。
|
|
110
|
+
*/
|
|
111
|
+
function genSeed(len = 20) {
|
|
112
|
+
const out = Buffer.alloc(len);
|
|
113
|
+
for (let i = 0; i < len; i++) {
|
|
114
|
+
out[i] = 33 + Math.floor(Math.random() * 94);
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function okPacket(affectedRows = 0, insertId = 0, status = SERVER_STATUS_AUTOCOMMIT) { const b = new PacketBuilder();
|
|
120
|
+
b.byte(0x00);
|
|
121
|
+
b.raw(encodeLenenc(affectedRows));
|
|
122
|
+
b.raw(encodeLenenc(insertId));
|
|
123
|
+
b.int16(status);
|
|
124
|
+
b.int16(0);
|
|
125
|
+
return b.build();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function eofPacket() {
|
|
129
|
+
const b = new PacketBuilder();
|
|
130
|
+
b.byte(0xfe);
|
|
131
|
+
b.int16(0);
|
|
132
|
+
b.int16(SERVER_STATUS_AUTOCOMMIT);
|
|
133
|
+
return b.build();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function errPacket(errno, message, sqlState = 'HY000') {
|
|
137
|
+
const b = new PacketBuilder();
|
|
138
|
+
b.byte(0xff);
|
|
139
|
+
b.int16(errno);
|
|
140
|
+
b.byte(0x23);
|
|
141
|
+
b.string(sqlState);
|
|
142
|
+
b.string(String(message).slice(0, 200));
|
|
143
|
+
return b.build();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function toMysqlErrno(e) {
|
|
147
|
+
if (e && typeof e.code === 'number') return e.code;
|
|
148
|
+
const m = e && e.message ? String(e.message).match(/^(ER_[A-Z_]+)/) : null;
|
|
149
|
+
if (m) {
|
|
150
|
+
const known = {
|
|
151
|
+
ER_DUP_ENTRY: 1062,
|
|
152
|
+
ER_NO_SUCH_TABLE: 1146,
|
|
153
|
+
ER_TABLE_EXISTS: 1050,
|
|
154
|
+
ER_TABLE_EXISTS_ERROR: 1050,
|
|
155
|
+
ER_PARSE_ERROR: 1064,
|
|
156
|
+
ER_BAD_FIELD_ERROR: 1054,
|
|
157
|
+
ER_BAD_NULL_ERROR: 1048,
|
|
158
|
+
ER_ACCESS_DENIED_ERROR: 1045,
|
|
159
|
+
ER_DBACCESS_DENIED_ERROR: 1044,
|
|
160
|
+
ER_BAD_DB_ERROR: 1049,
|
|
161
|
+
ER_WRONG_DB_NAME: 1102,
|
|
162
|
+
ER_WRONG_TABLE_NAME: 1103,
|
|
163
|
+
ER_WRONG_COLUMN_NAME: 1166,
|
|
164
|
+
ER_DATA_TOO_LONG: 1406,
|
|
165
|
+
ER_OUT_OF_RANGE: 1264,
|
|
166
|
+
ER_CHECK_CONSTRAINT: 3819,
|
|
167
|
+
ER_NO_DEFAULT_FOR_FIELD: 1364,
|
|
168
|
+
ER_CANT_DROP_FIELD_OR_KEY: 1091,
|
|
169
|
+
ER_CANT_DROP_DATABASE: 1008,
|
|
170
|
+
ER_EMPTY_QUERY: 1065,
|
|
171
|
+
ER_UNKNOWN_TABLE: 1109,
|
|
172
|
+
ER_NON_UNIQ_ERROR: 1052,
|
|
173
|
+
ER_WRONG_FIELD_WITH_GROUP: 1055,
|
|
174
|
+
ER_WRONG_VALUE_COUNT_ON_ROW: 1136,
|
|
175
|
+
ER_MISSING_TABLE: 1052,
|
|
176
|
+
ER_SP_DOES_NOT_EXIST: 1305,
|
|
177
|
+
ER_NOT_SUPPORTED_YET: 1235,
|
|
178
|
+
ER_LOCK_DEADLOCK: 1213,
|
|
179
|
+
ER_LOCK_WAIT_TIMEOUT: 1205,
|
|
180
|
+
ER_UNKNOWN_ERROR: 1105,
|
|
181
|
+
};
|
|
182
|
+
if (known[m[1]]) return known[m[1]];
|
|
183
|
+
}
|
|
184
|
+
return 1105;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function columnDefinition(column) {
|
|
188
|
+
const b = new PacketBuilder();
|
|
189
|
+
b.raw(encodeLenencString('def'));
|
|
190
|
+
b.raw(encodeLenencString(column.table || ''));
|
|
191
|
+
b.raw(encodeLenencString(column.table || ''));
|
|
192
|
+
b.raw(encodeLenencString(column.table || ''));
|
|
193
|
+
b.raw(encodeLenencString(column.name || ''));
|
|
194
|
+
b.raw(encodeLenencString(column.name || ''));
|
|
195
|
+
b.raw(encodeLenenc(0x0c));
|
|
196
|
+
b.int16(column.charset || CHARSET_UTF8);
|
|
197
|
+
b.int32(column.length || 1024);
|
|
198
|
+
b.byte(column.type !== undefined ? column.type : MYSQL_TYPE_VAR_STRING);
|
|
199
|
+
b.int16(0);
|
|
200
|
+
b.byte(0);
|
|
201
|
+
b.int16(0);
|
|
202
|
+
return b.build();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function columnTypeFromSchema(def) {
|
|
206
|
+
const type = def && def.type ? String(def.type).toLowerCase() : 'string';
|
|
207
|
+
if (type === 'integer' || type === 'int' || type === 'bigint') return MYSQL_TYPE_LONG;
|
|
208
|
+
if (type === 'number' || type === 'float' || type === 'double' || type === 'real') return MYSQL_TYPE_DOUBLE;
|
|
209
|
+
if (type === 'boolean' || type === 'bool') return MYSQL_TYPE_TINY;
|
|
210
|
+
if (type === 'date') return MYSQL_TYPE_DATE;
|
|
211
|
+
if (type === 'datetime' || type === 'timestamp') return MYSQL_TYPE_DATETIME;
|
|
212
|
+
if (type === 'object' || type === 'array') return MYSQL_TYPE_JSON;
|
|
213
|
+
if (type === 'binary') return MYSQL_TYPE_BLOB;
|
|
214
|
+
return MYSQL_TYPE_VAR_STRING;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function resultSetPacket(result, tableSchema, baseSeq, rawRow) {
|
|
218
|
+
const packets = [];
|
|
219
|
+
let sequence = baseSeq || 0;
|
|
220
|
+
const push = (buf) => {
|
|
221
|
+
const header = Buffer.alloc(4);
|
|
222
|
+
header.writeUIntLE(buf.length, 0, 3);
|
|
223
|
+
header[3] = sequence;
|
|
224
|
+
sequence++;
|
|
225
|
+
packets.push(header);
|
|
226
|
+
packets.push(buf);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const inferType = (name, value) => {
|
|
230
|
+
if (tableSchema && tableSchema[name]) return columnTypeFromSchema(tableSchema[name]);
|
|
231
|
+
if (value === null || value === undefined) return MYSQL_TYPE_NULL;
|
|
232
|
+
if (typeof value === 'number') return Number.isInteger(value) ? MYSQL_TYPE_LONGLONG : MYSQL_TYPE_DOUBLE;
|
|
233
|
+
if (typeof value === 'boolean') return MYSQL_TYPE_TINY;
|
|
234
|
+
if (typeof value === 'object') return MYSQL_TYPE_JSON;
|
|
235
|
+
return MYSQL_TYPE_VAR_STRING;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
push(encodeLenenc((result.columns || []).length));
|
|
239
|
+
const sampleRow = (result.rows && result.rows[0]) || null;
|
|
240
|
+
for (let ci = 0; ci < (result.columns || []).length; ci++) {
|
|
241
|
+
const name = result.columns[ci];
|
|
242
|
+
let sample;
|
|
243
|
+
if (sampleRow !== null) {
|
|
244
|
+
if (Array.isArray(sampleRow)) sample = sampleRow[ci];
|
|
245
|
+
else sample = sampleRow[name];
|
|
246
|
+
}
|
|
247
|
+
const col = {
|
|
248
|
+
name,
|
|
249
|
+
table: result.table || '',
|
|
250
|
+
type: inferType(name, sample),
|
|
251
|
+
length: 1024,
|
|
252
|
+
};
|
|
253
|
+
push(columnDefinition(col));
|
|
254
|
+
}
|
|
255
|
+
push(eofPacket());
|
|
256
|
+
|
|
257
|
+
for (const row of result.rows || []) {
|
|
258
|
+
const parts = [];
|
|
259
|
+
for (let i = 0; i < (result.columns || []).length; i++) {
|
|
260
|
+
const v = row[i];
|
|
261
|
+
if (v === null || v === undefined) {
|
|
262
|
+
parts.push(Buffer.from([0xfb]));
|
|
263
|
+
} else if (typeof v === 'boolean') {
|
|
264
|
+
parts.push(encodeLenencString(v ? '1' : '0'));
|
|
265
|
+
} else if (typeof v === 'object') {
|
|
266
|
+
parts.push(encodeLenencString(JSON.stringify(v)));
|
|
267
|
+
} else {
|
|
268
|
+
parts.push(encodeLenencString(v));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
push(Buffer.concat(parts));
|
|
272
|
+
}
|
|
273
|
+
push(eofPacket());
|
|
274
|
+
return { packets, sequence };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function binaryResultSetPacket(result, tableSchema, baseSeq) {
|
|
278
|
+
const packets = [];
|
|
279
|
+
let sequence = baseSeq || 0;
|
|
280
|
+
const push = (buf) => {
|
|
281
|
+
const header = Buffer.alloc(4);
|
|
282
|
+
header.writeUIntLE(buf.length, 0, 3);
|
|
283
|
+
header[3] = sequence;
|
|
284
|
+
sequence++;
|
|
285
|
+
packets.push(header);
|
|
286
|
+
packets.push(buf);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const inferType = (name, value) => {
|
|
290
|
+
if (tableSchema && tableSchema[name]) return columnTypeFromSchema(tableSchema[name]);
|
|
291
|
+
if (value === null || value === undefined) return MYSQL_TYPE_NULL;
|
|
292
|
+
if (typeof value === 'number') return Number.isInteger(value) ? MYSQL_TYPE_LONGLONG : MYSQL_TYPE_DOUBLE;
|
|
293
|
+
if (typeof value === 'boolean') return MYSQL_TYPE_TINY;
|
|
294
|
+
if (typeof value === 'object') return MYSQL_TYPE_JSON;
|
|
295
|
+
return MYSQL_TYPE_VAR_STRING;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const cols = result.columns || [];
|
|
299
|
+
push(encodeLenenc(cols.length));
|
|
300
|
+
const sampleRow = (result.rows && result.rows[0]) || null;
|
|
301
|
+
const types = [];
|
|
302
|
+
for (let ci = 0; ci < cols.length; ci++) {
|
|
303
|
+
let sample;
|
|
304
|
+
if (sampleRow !== null) {
|
|
305
|
+
if (Array.isArray(sampleRow)) sample = sampleRow[ci];
|
|
306
|
+
else sample = sampleRow[cols[ci]];
|
|
307
|
+
}
|
|
308
|
+
const t = inferType(cols[ci], sample);
|
|
309
|
+
types.push(t);
|
|
310
|
+
push(columnDefinition({ name: cols[ci], table: result.table || '', type: t, length: 1024 }));
|
|
311
|
+
}
|
|
312
|
+
push(eofPacket());
|
|
313
|
+
|
|
314
|
+
const encodeValue = (v, type) => {
|
|
315
|
+
if (v === null || v === undefined) return null;
|
|
316
|
+
if (type === MYSQL_TYPE_LONG || type === MYSQL_TYPE_LONGLONG || type === MYSQL_TYPE_DOUBLE) {
|
|
317
|
+
const num = Number(v);
|
|
318
|
+
if (type === MYSQL_TYPE_DOUBLE) {
|
|
319
|
+
if (!Number.isFinite(num)) return encodeLenencString(String(v));
|
|
320
|
+
const b = Buffer.alloc(8); b.writeDoubleLE(num, 0); return b;
|
|
321
|
+
}
|
|
322
|
+
if (Number.isInteger(num) && num <= 2147483647 && num >= -2147483648) {
|
|
323
|
+
const b = Buffer.alloc(4); b.writeInt32LE(num, 0); return b;
|
|
324
|
+
}
|
|
325
|
+
if (!Number.isFinite(num)) return encodeLenencString(String(v));
|
|
326
|
+
const b = Buffer.alloc(8); b.writeBigInt64LE(BigInt(Math.trunc(num)), 0); return b;
|
|
327
|
+
}
|
|
328
|
+
if (type === MYSQL_TYPE_TINY) {
|
|
329
|
+
const b = Buffer.alloc(1); b.writeInt8(v ? 1 : 0, 0); return b;
|
|
330
|
+
}
|
|
331
|
+
if (type === MYSQL_TYPE_DATE || type === MYSQL_TYPE_DATETIME) {
|
|
332
|
+
const s = String(v);
|
|
333
|
+
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2}))?/);
|
|
334
|
+
if (!m) return encodeLenencString(String(v));
|
|
335
|
+
const hasTime = !!m[4];
|
|
336
|
+
const buf = hasTime ? Buffer.alloc(8) : Buffer.alloc(5);
|
|
337
|
+
let off = 0;
|
|
338
|
+
buf[off++] = hasTime ? 7 : 4;
|
|
339
|
+
buf.writeUInt16LE(parseInt(m[1], 10), off); off += 2;
|
|
340
|
+
buf[off++] = parseInt(m[2], 10);
|
|
341
|
+
buf[off++] = parseInt(m[3], 10);
|
|
342
|
+
if (hasTime) {
|
|
343
|
+
buf[off++] = parseInt(m[4], 10);
|
|
344
|
+
buf[off++] = parseInt(m[5], 10);
|
|
345
|
+
buf[off++] = parseInt(m[6], 10);
|
|
346
|
+
}
|
|
347
|
+
return buf;
|
|
348
|
+
}
|
|
349
|
+
return encodeLenencString(typeof v === 'object' ? JSON.stringify(v) : v);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
for (const row of result.rows || []) {
|
|
353
|
+
const parts = [Buffer.from([0x00])];
|
|
354
|
+
const nb = Buffer.alloc(Math.ceil(cols.length / 8));
|
|
355
|
+
const encoded = [];
|
|
356
|
+
for (let i = 0; i < cols.length; i++) {
|
|
357
|
+
const v = Array.isArray(row) ? row[i] : row[cols[i]];
|
|
358
|
+
if (v === null || v === undefined) {
|
|
359
|
+
nb[Math.floor(i / 8)] |= (1 << (i % 8));
|
|
360
|
+
encoded.push(null);
|
|
361
|
+
} else {
|
|
362
|
+
encoded.push(encodeValue(v, types[i]));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
parts.push(nb);
|
|
366
|
+
for (const e of encoded) if (e !== null) parts.push(e);
|
|
367
|
+
push(Buffer.concat(parts));
|
|
368
|
+
}
|
|
369
|
+
push(eofPacket());
|
|
370
|
+
return { packets, sequence };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
class MysqlConnection {
|
|
374
|
+
constructor(socket, server) {
|
|
375
|
+
this.socket = socket;
|
|
376
|
+
this.server = server;
|
|
377
|
+
this.connectionId = ++server._connectionCounter;
|
|
378
|
+
this.seed = genSeed(20);
|
|
379
|
+
this.buffer = Buffer.alloc(0);
|
|
380
|
+
this.sequence = 0;
|
|
381
|
+
this.authenticated = false;
|
|
382
|
+
this.user = null;
|
|
383
|
+
this.authFails = 0;
|
|
384
|
+
this.multiStatements = false;
|
|
385
|
+
this._stmts = new Map();
|
|
386
|
+
this._stmtSeq = 0;
|
|
387
|
+
this.session = {
|
|
388
|
+
lastInsertId: 0,
|
|
389
|
+
rowCount: 0,
|
|
390
|
+
foundRows: 0,
|
|
391
|
+
connectionId: server._connectionCounter,
|
|
392
|
+
currentDb: null,
|
|
393
|
+
sysvars: {},
|
|
394
|
+
};
|
|
395
|
+
if (server.handshakeTimeout > 0) {
|
|
396
|
+
this._authTimer = setTimeout(() => {
|
|
397
|
+
if (!this.authenticated) {
|
|
398
|
+
this.server._onSecurityEvent({ type: 'auth-timeout', user: this.user, remote: socket.remoteAddress });
|
|
399
|
+
socket.destroy();
|
|
400
|
+
}
|
|
401
|
+
}, server.handshakeTimeout);
|
|
402
|
+
this._authTimer.unref();
|
|
403
|
+
}
|
|
404
|
+
this.socket.on('data', chunk => this._onData(chunk));
|
|
405
|
+
this.socket.on('error', () => {});
|
|
406
|
+
this._armIdleTimer();
|
|
407
|
+
this._send(handshakePacket(this.connectionId, this.seed));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
_armIdleTimer() {
|
|
411
|
+
if (this._idleTimer) { clearTimeout(this._idleTimer); this._idleTimer = null; }
|
|
412
|
+
const ttl = this.server.idleTimeout;
|
|
413
|
+
if (!this.authenticated) return;
|
|
414
|
+
if (ttl <= 0) return;
|
|
415
|
+
this._idleTimer = setTimeout(() => {
|
|
416
|
+
this.server._onSecurityEvent({ type: 'idle-timeout', user: this.user, remote: this.socket.remoteAddress });
|
|
417
|
+
this.socket.destroy();
|
|
418
|
+
}, ttl);
|
|
419
|
+
this._idleTimer.unref();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
_send(payload) {
|
|
423
|
+
if (!this.socket.writable) return;
|
|
424
|
+
const header = Buffer.alloc(4);
|
|
425
|
+
header.writeUIntLE(payload.length, 0, 3);
|
|
426
|
+
header[3] = this.sequence;
|
|
427
|
+
this.sequence = (this.sequence + 1) & 0xff;
|
|
428
|
+
this.socket.write(Buffer.concat([header, payload]));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
_onData(chunk) {
|
|
432
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
433
|
+
this._armIdleTimer();
|
|
434
|
+
if (this.buffer.length > this.server.maxPacketSize + 4) {
|
|
435
|
+
this._malicious('packet exceeds maxPacketSize (' + this.server.maxPacketSize + ')');
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
while (true) {
|
|
439
|
+
if (this.buffer.length < 4) return;
|
|
440
|
+
const len = this.buffer.readUIntLE(0, 3);
|
|
441
|
+
if (len > this.server.maxPacketSize) {
|
|
442
|
+
this._malicious('packet length ' + len + ' exceeds maxPacketSize');
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (this.buffer.length < 4 + len) return;
|
|
446
|
+
const seq = this.buffer[3];
|
|
447
|
+
const payload = this.buffer.slice(4, 4 + len);
|
|
448
|
+
this.buffer = this.buffer.slice(4 + len);
|
|
449
|
+
this._handlePacket(payload, seq);
|
|
450
|
+
if (this.socket.destroyed) return;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
_malicious(reason) {
|
|
455
|
+
this.socket.destroy();
|
|
456
|
+
this.server._onSecurityEvent({ type: 'malicious', reason, user: this.user, remote: this.socket.remoteAddress });
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
_handlePacket(payload, cmdSeq) {
|
|
460
|
+
if (!this.authenticated) {
|
|
461
|
+
this.sequence = (cmdSeq + 1) & 0xff;
|
|
462
|
+
this._handleAuth(payload);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
this.sequence = (cmdSeq + 1) & 0xff;
|
|
466
|
+
const cmd = payload[0];
|
|
467
|
+
const body = payload.slice(1);
|
|
468
|
+
try {
|
|
469
|
+
switch (cmd) {
|
|
470
|
+
case 0x01: this.socket.end(); break; // COM_QUIT
|
|
471
|
+
case 0x02: { // COM_INIT_DB
|
|
472
|
+
const db = body.toString('utf8');
|
|
473
|
+
if (!this.server._safeDbName(db)) {
|
|
474
|
+
this._malicious('COM_INIT_DB with invalid database name');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
this.server._switchDb(this, db)
|
|
478
|
+
.then(() => this._send(okPacket()))
|
|
479
|
+
.catch(e => this._send(errPacket(e.code || 1049, e.message)));
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
case 0x03: {
|
|
483
|
+
const sql = body.toString('utf8');
|
|
484
|
+
if (sql.length === 0) {
|
|
485
|
+
this._send(errPacket(1065, 'Query was empty'));
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
this._handleQuery(sql);
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
case 0x11: this._handleChangeUser(body, cmdSeq); break; // COM_CHANGE_USER
|
|
492
|
+
case 0x0e: this._send(okPacket()); break; // COM_PING
|
|
493
|
+
case 0x1f: this._send(okPacket()); break; // COM_RESET_CONNECTION
|
|
494
|
+
case 0x16: this._handleStmtPrepare(body); break; // COM_STMT_PREPARE
|
|
495
|
+
case 0x17: this._handleStmtExecute(body); break; // COM_STMT_EXECUTE
|
|
496
|
+
case 0x18: break; // COM_STMT_SEND_LONG_DATA (忽略)
|
|
497
|
+
case 0x19: this._handleStmtClose(body); break; // COM_STMT_CLOSE
|
|
498
|
+
case 0x1a: this._send(okPacket()); break; // COM_STMT_RESET
|
|
499
|
+
case 0x0a: { // COM_PROCESS_INFO
|
|
500
|
+
const seq = this.sequence;
|
|
501
|
+
this.sequence = (this.sequence + 1) & 0xff;
|
|
502
|
+
this._send(encodeLenenc(1));
|
|
503
|
+
this._send(columnDefinition({ name: 'Id', type: MYSQL_TYPE_LONG }));
|
|
504
|
+
this._send(eofPacket());
|
|
505
|
+
this._send(Buffer.from([0x31]));
|
|
506
|
+
this._send(eofPacket());
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
case 0x09: this._send(okPacket(0, 0, SERVER_STATUS_AUTOCOMMIT)); break; // COM_STATISTICS
|
|
510
|
+
default:
|
|
511
|
+
this._send(errPacket(1105, `Unsupported command: ${cmd}`));
|
|
512
|
+
}
|
|
513
|
+
} catch (e) {
|
|
514
|
+
this._send(errPacket(toMysqlErrno(e), e.message));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
_handleChangeUser(body, cmdSeq) {
|
|
519
|
+
(async () => {
|
|
520
|
+
try {
|
|
521
|
+
let pos = 0;
|
|
522
|
+
const userEnd = body.indexOf(0, pos);
|
|
523
|
+
if (userEnd === -1) throw new Error('malformed COM_CHANGE_USER');
|
|
524
|
+
const user = body.slice(pos, userEnd).toString('utf8');
|
|
525
|
+
pos = userEnd + 1;
|
|
526
|
+
const lenenc = parseLenenc(body, pos);
|
|
527
|
+
let authResponse = Buffer.alloc(0);
|
|
528
|
+
if (lenenc.value !== null) {
|
|
529
|
+
authResponse = body.slice(pos + lenenc.size, pos + lenenc.size + lenenc.value);
|
|
530
|
+
pos += lenenc.size + lenenc.value;
|
|
531
|
+
} else {
|
|
532
|
+
pos += lenenc.size;
|
|
533
|
+
}
|
|
534
|
+
let db = null;
|
|
535
|
+
if (pos < body.length) {
|
|
536
|
+
const dbEnd = body.indexOf(0, pos);
|
|
537
|
+
if (dbEnd !== -1) {
|
|
538
|
+
db = body.slice(pos, dbEnd).toString('utf8');
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const valid = this.server._checkAuth(user, authResponse, this.seed);
|
|
542
|
+
if (!valid) {
|
|
543
|
+
this._send(errPacket(1045, `Access denied for user '${user}'`));
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (db) {
|
|
547
|
+
if (!this.server._canAccessDb(user, db)) {
|
|
548
|
+
this._send(errPacket(1044, `Access denied for user '${user}' to database '${db}'`));
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
await this.server._getDatabase(db);
|
|
553
|
+
} catch (e) {
|
|
554
|
+
this._send(errPacket(1049, `Unknown database '${db}'`));
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
this.user = user;
|
|
559
|
+
this.currentDb = db;
|
|
560
|
+
this._send(okPacket());
|
|
561
|
+
} catch (e) {
|
|
562
|
+
this._send(errPacket(toMysqlErrno(e), e.message));
|
|
563
|
+
}
|
|
564
|
+
})();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async _handleAuth(payload) {
|
|
568
|
+
try {
|
|
569
|
+
const caps = payload.readUInt32LE(0);
|
|
570
|
+
let pos = 32; // 4 caps + 4 maxpacket + 1 charset + 23 reserved
|
|
571
|
+
const userEnd = payload.indexOf(0, pos);
|
|
572
|
+
this.user = payload.slice(pos, userEnd).toString('utf8');
|
|
573
|
+
pos = userEnd + 1;
|
|
574
|
+
let authResponse = Buffer.alloc(0);
|
|
575
|
+
if (caps & CLIENT_SECURE_CONNECTION) {
|
|
576
|
+
const lenenc = parseLenenc(payload, pos);
|
|
577
|
+
if (lenenc.value === null) { pos += lenenc.size; }
|
|
578
|
+
else {
|
|
579
|
+
authResponse = payload.slice(pos + lenenc.size, pos + lenenc.size + lenenc.value);
|
|
580
|
+
pos += lenenc.size + lenenc.value;
|
|
581
|
+
}
|
|
582
|
+
} else {
|
|
583
|
+
const end = payload.indexOf(0, pos);
|
|
584
|
+
if (end !== -1) {
|
|
585
|
+
authResponse = payload.slice(pos, end);
|
|
586
|
+
pos = end + 1;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
let db = null;
|
|
590
|
+
if (caps & CLIENT_CONNECT_WITH_DB) {
|
|
591
|
+
const dbEnd = payload.indexOf(0, pos);
|
|
592
|
+
if (dbEnd !== -1) {
|
|
593
|
+
db = payload.slice(pos, dbEnd).toString('utf8');
|
|
594
|
+
pos = dbEnd + 1;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
this.currentDb = db;
|
|
598
|
+
|
|
599
|
+
const valid = this.server._checkAuth(this.user, authResponse, this.seed);
|
|
600
|
+
if (!valid) {
|
|
601
|
+
this.authFails++;
|
|
602
|
+
this.server._onSecurityEvent({ type: 'auth-fail', user: this.user, fails: this.authFails, remote: this.socket.remoteAddress });
|
|
603
|
+
if (this.authFails >= this.server.maxAuthFails) {
|
|
604
|
+
this.socket.destroy();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
this._send(errPacket(1045, `Access denied for user '${this.user}'`));
|
|
608
|
+
this.socket.end();
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
this.multiStatements = !!(caps & CLIENT_MULTI_STATEMENTS);
|
|
612
|
+
this.authenticated = true;
|
|
613
|
+
if (this._authTimer) { clearTimeout(this._authTimer); this._authTimer = null; }
|
|
614
|
+
this._armIdleTimer();
|
|
615
|
+
if (db) {
|
|
616
|
+
if (!this.server._canAccessDb(this.user, db)) {
|
|
617
|
+
this._send(errPacket(1044, `Access denied for user '${this.user}' to database '${db}'`));
|
|
618
|
+
this.socket.end();
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
if (!this.server._dbExists(db)) await this.server.createDatabase(db);
|
|
623
|
+
await this.server._getDatabase(db);
|
|
624
|
+
this.currentDb = db;
|
|
625
|
+
} catch (e) {
|
|
626
|
+
this._send(errPacket(1049, `Unknown database '${db}'`));
|
|
627
|
+
this.socket.end();
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
this._send(okPacket());
|
|
632
|
+
} catch (e) {
|
|
633
|
+
this._send(errPacket(1105, 'auth failed: ' + e.message));
|
|
634
|
+
this.socket.end();
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
countPlaceholders(sql) {
|
|
639
|
+
let n = 0;
|
|
640
|
+
let inStr = null;
|
|
641
|
+
for (let i = 0; i < sql.length; i++) {
|
|
642
|
+
const c = sql[i];
|
|
643
|
+
if (inStr) {
|
|
644
|
+
if (c === '\\' && i + 1 < sql.length) { i++; continue; }
|
|
645
|
+
if (c === inStr) inStr = null;
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (c === "'" || c === '"' || c === '`') { inStr = c; continue; }
|
|
649
|
+
if (c === '?' && sql[i + 1] === '?') { i++; continue; }
|
|
650
|
+
if (c === '?') n++;
|
|
651
|
+
}
|
|
652
|
+
return n;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
_handleStmtPrepare(body) {
|
|
656
|
+
try {
|
|
657
|
+
const sql = body.toString('utf8');
|
|
658
|
+
const numParams = this.countPlaceholders(sql);
|
|
659
|
+
let numColumns = 0;
|
|
660
|
+
try {
|
|
661
|
+
const stmt = parseSQL(sql);
|
|
662
|
+
if (stmt && stmt.type === 'select' && stmt.columns) numColumns = stmt.columns.length;
|
|
663
|
+
} catch (e) { /* 无法解析的 SQL 仍可 prepare */ }
|
|
664
|
+
const stmtId = ++this._stmtSeq;
|
|
665
|
+
this._stmts.set(stmtId, { sql, numParams, numColumns });
|
|
666
|
+
|
|
667
|
+
const pkt = Buffer.alloc(12);
|
|
668
|
+
let off = 0;
|
|
669
|
+
pkt[off++] = 0x00;
|
|
670
|
+
pkt.writeUInt32LE(stmtId, off); off += 4;
|
|
671
|
+
pkt.writeUInt16LE(numColumns, off); off += 2;
|
|
672
|
+
pkt.writeUInt16LE(numParams, off); off += 2;
|
|
673
|
+
pkt[off++] = 0x00;
|
|
674
|
+
pkt.writeUInt16LE(0, off);
|
|
675
|
+
this._send(pkt);
|
|
676
|
+
|
|
677
|
+
if (numParams > 0) {
|
|
678
|
+
for (let i = 0; i < numParams; i++) {
|
|
679
|
+
this._send(columnDefinition({ name: '?', type: MYSQL_TYPE_VAR_STRING }));
|
|
680
|
+
}
|
|
681
|
+
this._send(eofPacket());
|
|
682
|
+
}
|
|
683
|
+
if (numColumns > 0) {
|
|
684
|
+
for (let i = 0; i < numColumns; i++) {
|
|
685
|
+
this._send(columnDefinition({ name: 'col' + (i + 1), type: MYSQL_TYPE_VAR_STRING }));
|
|
686
|
+
}
|
|
687
|
+
this._send(eofPacket());
|
|
688
|
+
}
|
|
689
|
+
} catch (e) {
|
|
690
|
+
this._send(errPacket(toMysqlErrno(e), e.message));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
_handleStmtClose(body) {
|
|
695
|
+
if (body.length >= 4) {
|
|
696
|
+
const stmtId = body.readUInt32LE(0);
|
|
697
|
+
this._stmts.delete(stmtId);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
_handleStmtExecute(body) {
|
|
702
|
+
try {
|
|
703
|
+
if (body.length < 9) throw new Error('malformed COM_STMT_EXECUTE');
|
|
704
|
+
const stmtId = body.readUInt32LE(0);
|
|
705
|
+
const stmt = this._stmts.get(stmtId);
|
|
706
|
+
if (!stmt) {
|
|
707
|
+
this._send(errPacket(1243, 'Unknown prepared statement handler (' + stmtId + ') given to mysqld_stmt_execute'));
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const flags = body[4];
|
|
711
|
+
const iteration = body.readUInt32LE(5);
|
|
712
|
+
let values = null;
|
|
713
|
+
if (stmt.numParams > 0) {
|
|
714
|
+
let off = 9;
|
|
715
|
+
const numParams = stmt.numParams;
|
|
716
|
+
const nullBitmapLen = Math.ceil(numParams / 8);
|
|
717
|
+
if (body.length < off + nullBitmapLen) throw new Error('malformed COM_STMT_EXECUTE params');
|
|
718
|
+
const nullBitmap = body.slice(off, off + nullBitmapLen);
|
|
719
|
+
off += nullBitmapLen;
|
|
720
|
+
if (off >= body.length) throw new Error('malformed COM_STMT_EXECUTE params');
|
|
721
|
+
const newParamsBound = body[off];
|
|
722
|
+
off += 1;
|
|
723
|
+
let types = stmt.types;
|
|
724
|
+
if (newParamsBound & 0x01) {
|
|
725
|
+
if (body.length < off + numParams * 2) throw new Error('malformed COM_STMT_EXECUTE types');
|
|
726
|
+
types = [];
|
|
727
|
+
for (let i = 0; i < numParams; i++) types.push(body.readUInt16LE(off + i * 2));
|
|
728
|
+
stmt.types = types;
|
|
729
|
+
off += numParams * 2;
|
|
730
|
+
}
|
|
731
|
+
if (!types) throw new Error('Parameter types unknown for prepared statement');
|
|
732
|
+
values = [];
|
|
733
|
+
for (let i = 0; i < numParams; i++) {
|
|
734
|
+
const isNull = nullBitmap[Math.floor(i / 8)] & (1 << (i % 8));
|
|
735
|
+
if (isNull) { values.push(null); continue; }
|
|
736
|
+
const type = types[i] & 0xff;
|
|
737
|
+
const parsed = this._readParamValue(body, off, type);
|
|
738
|
+
values.push(parsed.value);
|
|
739
|
+
off = parsed.off;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
this._handleQuery(stmt.sql, values !== null && values.length > 0 ? values : undefined);
|
|
743
|
+
} catch (e) {
|
|
744
|
+
this._send(errPacket(toMysqlErrno(e), e.message));
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
_readParamValue(buf, off, type) {
|
|
749
|
+
switch (type) {
|
|
750
|
+
case MYSQL_TYPE_NULL: return { value: null, off };
|
|
751
|
+
case 0x01: { const v = buf.readInt8(off); return { value: v, off: off + 1 }; } // TINY
|
|
752
|
+
case 0x02: { const v = buf.readInt16LE(off); return { value: v, off: off + 2 }; } // SHORT
|
|
753
|
+
case 0x03: { const v = buf.readInt32LE(off); return { value: v, off: off + 4 }; } // LONG
|
|
754
|
+
case 0x08: { const v = buf.readBigInt64LE(off); return { value: Number(v), off: off + 8 }; } // LONGLONG
|
|
755
|
+
case 0x04: { const v = buf.readFloatLE(off); return { value: v, off: off + 4 }; } // FLOAT
|
|
756
|
+
case MYSQL_TYPE_DOUBLE: { const v = buf.readDoubleLE(off); return { value: v, off: off + 8 }; }
|
|
757
|
+
case 0x0a: case 0x07: case 0x0b: case MYSQL_TYPE_DATETIME: { // DATE/TIMESTAMP/DATETIME/TIME
|
|
758
|
+
const len = buf[off];
|
|
759
|
+
off += 1;
|
|
760
|
+
if (len === 0) return { value: null, off };
|
|
761
|
+
let value;
|
|
762
|
+
if (type === 0x0b) { // TIME
|
|
763
|
+
let sign = 1;
|
|
764
|
+
let p = off;
|
|
765
|
+
if (buf[p] !== 0) sign = -1;
|
|
766
|
+
p += 1;
|
|
767
|
+
const days = buf.readUInt32LE(p); p += 4;
|
|
768
|
+
const hour = buf[p++];
|
|
769
|
+
const min = buf[p++];
|
|
770
|
+
const sec = buf[p++];
|
|
771
|
+
value = sign * (days * 24 + hour) + ':' + String(min).padStart(2, '0') + ':' + String(sec).padStart(2, '0');
|
|
772
|
+
} else {
|
|
773
|
+
let p = off;
|
|
774
|
+
const year = buf.readUInt16LE(p); p += 2;
|
|
775
|
+
const month = buf[p++];
|
|
776
|
+
const day = buf[p++];
|
|
777
|
+
let hour = 0, minute = 0, second = 0;
|
|
778
|
+
if (len >= 7) { hour = buf[p++]; minute = buf[p++]; second = buf[p++]; }
|
|
779
|
+
value = String(year).padStart(4, '0') + '-' + String(month).padStart(2, '0') + '-' + String(day).padStart(2, '0');
|
|
780
|
+
if (len >= 7) value += ' ' + String(hour).padStart(2, '0') + ':' + String(minute).padStart(2, '0') + ':' + String(second).padStart(2, '0');
|
|
781
|
+
}
|
|
782
|
+
off += len;
|
|
783
|
+
return { value, off };
|
|
784
|
+
}
|
|
785
|
+
case 0xfc: case 0xfd: case 0xfe: case 0xf9: case 0xf6: { // BLOB / VAR_STRING / STRING / ...
|
|
786
|
+
const { value, off: noff } = readLenenc(buf, off);
|
|
787
|
+
return { value, off: noff };
|
|
788
|
+
}
|
|
789
|
+
case 0xf0: { // NULL
|
|
790
|
+
return { value: null, off };
|
|
791
|
+
}
|
|
792
|
+
default: {
|
|
793
|
+
const { value, off: noff } = readLenenc(buf, off);
|
|
794
|
+
return { value, off: noff };
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
_handleQuery(sql, values) {
|
|
800
|
+
(async () => {
|
|
801
|
+
try {
|
|
802
|
+
const hasParams = values !== undefined && values !== null && values.length > 0;
|
|
803
|
+
const processedSql = hasParams ? applyParams(sql, values) : sql;
|
|
804
|
+
const statements = splitStatements(processedSql);
|
|
805
|
+
if (statements.length > 1 && !this.multiStatements) {
|
|
806
|
+
throw new Error(`too many statements (${statements.length} > 1)`);
|
|
807
|
+
}
|
|
808
|
+
for (const stmtSql of statements) {
|
|
809
|
+
const stmt = parseSQL(stmtSql);
|
|
810
|
+
switch (stmt.type) {
|
|
811
|
+
case 'use': {
|
|
812
|
+
if (!this.server._canAccessDb(this.user, stmt.database)) {
|
|
813
|
+
this._send(errPacket(1044, `Access denied for user '${this.user}' to database '${stmt.database}'`));
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
await this.server._getDatabase(stmt.database);
|
|
818
|
+
this.currentDb = stmt.database;
|
|
819
|
+
this._send(okPacket());
|
|
820
|
+
} catch (e) {
|
|
821
|
+
this._send(errPacket(1049, `Unknown database '${stmt.database}'`));
|
|
822
|
+
}
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
case 'showDatabases': {
|
|
826
|
+
const dbs = await this.server.listDatabases();
|
|
827
|
+
const allowed = this.server._userDbList(this.user);
|
|
828
|
+
const names = allowed === null ? dbs : dbs.filter(n => allowed.includes(n));
|
|
829
|
+
const { packets, sequence } = resultSetPacket({
|
|
830
|
+
type: 'showDatabases', columns: ['Database'], rows: names.map(n => [n]),
|
|
831
|
+
}, null, this.sequence);
|
|
832
|
+
this.sequence = sequence;
|
|
833
|
+
this.socket.write(Buffer.concat(packets));
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
case 'createDatabase': {
|
|
837
|
+
if (!this.server._canAccessDb(this.user, stmt.database)) {
|
|
838
|
+
this._send(errPacket(1044, `Access denied for user '${this.user}' to database '${stmt.database}'`));
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
await this.server.createDatabase(stmt.database, { ifNotExists: stmt.ifNotExists });
|
|
842
|
+
this._send(okPacket());
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
case 'dropDatabase': {
|
|
846
|
+
if (!this.server._canAccessDb(this.user, stmt.database)) {
|
|
847
|
+
this._send(errPacket(1044, `Access denied for user '${this.user}' to database '${stmt.database}'`));
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
await this.server.dropDatabase(stmt.database, { ifExists: stmt.ifExists });
|
|
851
|
+
this._send(okPacket());
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
default:
|
|
855
|
+
break;
|
|
856
|
+
}
|
|
857
|
+
const routed = await this.server._route(stmt, stmtSql, this.currentDb, this.user);
|
|
858
|
+
const engine = routed.engine;
|
|
859
|
+
const sqlToRun = routed.sql;
|
|
860
|
+
const r = await executeSQL(engine, sqlToRun, {
|
|
861
|
+
allowComments: this.server.allowComments,
|
|
862
|
+
safety: this.server.safety,
|
|
863
|
+
maxStatements: 1,
|
|
864
|
+
session: this.session,
|
|
865
|
+
});
|
|
866
|
+
if (r.type === 'select' || r.type === 'showTables' || r.type === 'showDatabases' || r.type === 'describe'
|
|
867
|
+
|| r.type === 'showColumns' || r.type === 'showIndex' || r.type === 'showCreateTable'
|
|
868
|
+
|| r.type === 'showVariables' || r.type === 'showStatus' || r.type === 'showGrants' || r.type === 'showWarnings') {
|
|
869
|
+
this.session.foundRows = (r.raw && r.raw.length !== undefined) ? r.raw.length : (r.rows ? r.rows.length : 0);
|
|
870
|
+
let schema = null;
|
|
871
|
+
if (r.table) {
|
|
872
|
+
schema = engine.getTableSchema
|
|
873
|
+
? await engine.getTableSchema(r.table)
|
|
874
|
+
: (engine._schemas ? engine._schemas[r.table] : null);
|
|
875
|
+
}
|
|
876
|
+
const { packets, sequence } = hasParams
|
|
877
|
+
? binaryResultSetPacket(r, schema, this.sequence)
|
|
878
|
+
: resultSetPacket(r, schema, this.sequence);
|
|
879
|
+
this.sequence = sequence;
|
|
880
|
+
this.socket.write(Buffer.concat(packets));
|
|
881
|
+
} else {
|
|
882
|
+
if (r.type === 'insert') {
|
|
883
|
+
this.session.lastInsertId = r.insertId;
|
|
884
|
+
this.session.rowCount = r.affectedRows;
|
|
885
|
+
} else if (r.type === 'update' || r.type === 'delete' || r.type === 'truncate') {
|
|
886
|
+
this.session.rowCount = r.affectedRows;
|
|
887
|
+
}
|
|
888
|
+
this._send(okPacket(r.affectedRows || 0, r.insertId || 0));
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
} catch (e) {
|
|
892
|
+
this._send(errPacket(toMysqlErrno(e), e.message));
|
|
893
|
+
}
|
|
894
|
+
})();
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
class MysqlServer {
|
|
899
|
+
constructor(options = {}) {
|
|
900
|
+
this.options = options;
|
|
901
|
+
this.port = options.port ?? 3306;
|
|
902
|
+
this.host = options.host || '127.0.0.1';
|
|
903
|
+
this.user = options.user || null;
|
|
904
|
+
this.password = options.password || null;
|
|
905
|
+
this.auth = options.auth || null;
|
|
906
|
+
this.safety = options.safety !== false;
|
|
907
|
+
this.allowComments = options.allowComments === true;
|
|
908
|
+
this.maxPacketSize = options.maxPacketSize || 1024 * 1024;
|
|
909
|
+
this.handshakeTimeout = options.handshakeTimeout != null ? options.handshakeTimeout : 10000;
|
|
910
|
+
this.idleTimeout = options.idleTimeout != null ? options.idleTimeout : 300000;
|
|
911
|
+
this.maxAuthFails = options.maxAuthFails || 3;
|
|
912
|
+
this.maxConnections = options.maxConnections || 128;
|
|
913
|
+
this._engine = null;
|
|
914
|
+
this._ownEngine = false;
|
|
915
|
+
this._databases = new Map();
|
|
916
|
+
this._dbDir = options.dataDir && options.dataDir !== ':memory:' ? path.resolve(options.dataDir) : null;
|
|
917
|
+
this._defaultDbName = options.defaultDatabase || 'default';
|
|
918
|
+
this._connectionCounter = 0;
|
|
919
|
+
this._sockets = new Set();
|
|
920
|
+
this._securityHandler = typeof options.onSecurityEvent === 'function' ? options.onSecurityEvent : null;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
_onSecurityEvent(event) {
|
|
924
|
+
if (this._securityHandler) {
|
|
925
|
+
try { this._securityHandler(event); } catch (e) {}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
async _getEngine(dbName) {
|
|
930
|
+
if (dbName) return this._getDatabase(dbName);
|
|
931
|
+
if (this._dbDir) {
|
|
932
|
+
return this._getDatabase(this._defaultDbName, { autoCreate: true });
|
|
933
|
+
}
|
|
934
|
+
if (this._engine) return this._engine;
|
|
935
|
+
if (this.options.engine || this.options.database) {
|
|
936
|
+
this._engine = this.options.engine || this.options.database;
|
|
937
|
+
if (typeof this._engine.start === 'function') await this._engine.start();
|
|
938
|
+
} else {
|
|
939
|
+
this._engine = new Database(this.options.filename || ':memory:');
|
|
940
|
+
if (typeof this._engine.start === 'function') await this._engine.start();
|
|
941
|
+
this._ownEngine = true;
|
|
942
|
+
}
|
|
943
|
+
return this._engine;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
_dbExists(name) {
|
|
947
|
+
if (this._databases.has(name)) return true;
|
|
948
|
+
const dir = this._dbPath(name);
|
|
949
|
+
return !!(dir && fs.existsSync(dir));
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// 语句路由:解析库前缀(db.table)与 SHOW TABLES FROM db,
|
|
953
|
+
// 决定执行引擎,并把 db.table 改写为 table 后交给对应库引擎执行。
|
|
954
|
+
async _route(stmt, sql, currentDb, user) {
|
|
955
|
+
if (stmt.type === 'showTables' && stmt.database) {
|
|
956
|
+
if (user && this.auth && !this._canAccessDb(user, stmt.database)) {
|
|
957
|
+
const err = new Error(`Access denied for user '${user}' to database '${stmt.database}'`);
|
|
958
|
+
err.code = 1044;
|
|
959
|
+
throw err;
|
|
960
|
+
}
|
|
961
|
+
return { engine: await this._getDatabase(stmt.database), sql };
|
|
962
|
+
}
|
|
963
|
+
const tables = [];
|
|
964
|
+
const collect = (s) => {
|
|
965
|
+
if (!s) return;
|
|
966
|
+
if (s.type === 'createTable') tables.push(s.name);
|
|
967
|
+
else if (s.type === 'dropTable') tables.push(s.table);
|
|
968
|
+
else if (s.type === 'insert') tables.push(s.name);
|
|
969
|
+
else if (s.type === 'update') tables.push(s.table);
|
|
970
|
+
else if (s.type === 'delete') tables.push(s.table);
|
|
971
|
+
else if (s.type === 'describe') tables.push(s.table);
|
|
972
|
+
else if (s.type === 'select' && s.from) {
|
|
973
|
+
for (const t of s.from.tables) tables.push(t.table);
|
|
974
|
+
for (const j of s.from.joins) tables.push(j.item.table);
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
collect(stmt);
|
|
978
|
+
const isInfoSchema = tables.some(t => t && String(t).toLowerCase().startsWith('information_schema.'));
|
|
979
|
+
if (isInfoSchema) {
|
|
980
|
+
return { engine: await this._getEngine(currentDb), sql };
|
|
981
|
+
}
|
|
982
|
+
let db = null;
|
|
983
|
+
for (const t of tables) {
|
|
984
|
+
if (t && t.indexOf('.') !== -1) {
|
|
985
|
+
const d = t.split('.')[0];
|
|
986
|
+
if (db && d !== db) {
|
|
987
|
+
const err = new Error(`Cross-database references are not supported in one statement`);
|
|
988
|
+
err.code = 1105;
|
|
989
|
+
throw err;
|
|
990
|
+
}
|
|
991
|
+
db = d;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (db) {
|
|
995
|
+
if (user && this.auth && !this._canAccessDb(user, db)) {
|
|
996
|
+
const err = new Error(`Access denied for user '${user}' to database '${db}'`);
|
|
997
|
+
err.code = 1044;
|
|
998
|
+
throw err;
|
|
999
|
+
}
|
|
1000
|
+
if (!this._dbExists(db)) await this.createDatabase(db);
|
|
1001
|
+
const engine = await this._getDatabase(db);
|
|
1002
|
+
let s = sql;
|
|
1003
|
+
for (const t of tables) {
|
|
1004
|
+
if (t && t.indexOf('.') !== -1) {
|
|
1005
|
+
s = s.replace(new RegExp('\\b' + t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'g'), t.split('.')[1]);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return { engine, sql: s };
|
|
1009
|
+
}
|
|
1010
|
+
if (this._dbDir && !currentDb && this.auth && !this._canAccessDb(user, this._defaultDbName)) {
|
|
1011
|
+
const err = new Error('No database selected');
|
|
1012
|
+
err.code = 1046;
|
|
1013
|
+
throw err;
|
|
1014
|
+
}
|
|
1015
|
+
return { engine: await this._getEngine(currentDb), sql };
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
_safeDbName(name) {
|
|
1019
|
+
if (typeof name !== 'string' || name.length === 0 || name.length > 64) return false;
|
|
1020
|
+
if (!/^[a-zA-Z0-9_$.\-]+$/.test(name)) return false;
|
|
1021
|
+
if (name === '.' || name === '..' || name.includes('..')) return false;
|
|
1022
|
+
return true;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
_dbPath(name) {
|
|
1026
|
+
if (!this._dbDir) return null;
|
|
1027
|
+
if (!this._safeDbName(name)) return null;
|
|
1028
|
+
return path.join(this._dbDir, name);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
async _getDatabase(name, opts = {}) {
|
|
1032
|
+
if (!this._safeDbName(name)) {
|
|
1033
|
+
const err = new Error(`Unknown database '${name}'`);
|
|
1034
|
+
err.code = 1049;
|
|
1035
|
+
throw err;
|
|
1036
|
+
}
|
|
1037
|
+
if (this._databases.has(name)) return this._databases.get(name);
|
|
1038
|
+
const dir = this._dbPath(name);
|
|
1039
|
+
if (dir && !fs.existsSync(dir)) {
|
|
1040
|
+
if (opts.autoCreate) {
|
|
1041
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1042
|
+
} else {
|
|
1043
|
+
const err = new Error(`Unknown database '${name}'`);
|
|
1044
|
+
err.code = 1049;
|
|
1045
|
+
throw err;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
const engine = dir ? new Database(dir, { mode: 'hybrid' }) : new Database(':memory:');
|
|
1049
|
+
if (typeof engine.start === 'function') await engine.start();
|
|
1050
|
+
this._databases.set(name, engine);
|
|
1051
|
+
return engine;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
async listDatabases() {
|
|
1055
|
+
if (!this._dbDir) return ['jsql'];
|
|
1056
|
+
let names = [];
|
|
1057
|
+
if (fs.existsSync(this._dbDir)) {
|
|
1058
|
+
names = fs.readdirSync(this._dbDir).filter(n => {
|
|
1059
|
+
const p = path.join(this._dbDir, n);
|
|
1060
|
+
return fs.statSync(p).isDirectory() && this._safeDbName(n);
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
for (const n of this._databases.keys()) {
|
|
1064
|
+
if (!names.includes(n)) names.push(n);
|
|
1065
|
+
}
|
|
1066
|
+
return names.sort();
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
async createDatabase(name, opts = {}) {
|
|
1070
|
+
if (!this._safeDbName(name)) {
|
|
1071
|
+
const err = new Error(`Invalid database name '${name}'`);
|
|
1072
|
+
err.code = 1105;
|
|
1073
|
+
throw err;
|
|
1074
|
+
}
|
|
1075
|
+
if (this._databases.has(name)) {
|
|
1076
|
+
if (opts.ifNotExists) return this._databases.get(name);
|
|
1077
|
+
const err = new Error(`Can't create database '${name}'; database exists`);
|
|
1078
|
+
err.code = 1007;
|
|
1079
|
+
throw err;
|
|
1080
|
+
}
|
|
1081
|
+
const dir = this._dbPath(name);
|
|
1082
|
+
if (dir && fs.existsSync(dir)) {
|
|
1083
|
+
if (opts.ifNotExists) return this._getDatabase(name);
|
|
1084
|
+
const err = new Error(`Can't create database '${name}'; database exists`);
|
|
1085
|
+
err.code = 1007;
|
|
1086
|
+
throw err;
|
|
1087
|
+
}
|
|
1088
|
+
if (dir) fs.mkdirSync(dir, { recursive: true });
|
|
1089
|
+
const engine = dir ? new Database(dir, { mode: 'hybrid' }) : new Database(':memory:');
|
|
1090
|
+
if (typeof engine.start === 'function') await engine.start();
|
|
1091
|
+
this._databases.set(name, engine);
|
|
1092
|
+
return engine;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
async dropDatabase(name, opts = {}) {
|
|
1096
|
+
if (!this._safeDbName(name)) {
|
|
1097
|
+
const err = new Error(`Unknown database '${name}'`);
|
|
1098
|
+
err.code = 1049;
|
|
1099
|
+
throw err;
|
|
1100
|
+
}
|
|
1101
|
+
const engine = this._databases.get(name);
|
|
1102
|
+
if (!engine) {
|
|
1103
|
+
if (opts.ifExists) return;
|
|
1104
|
+
const err = new Error(`Can't drop database '${name}'; database doesn't exist`);
|
|
1105
|
+
err.code = 1008;
|
|
1106
|
+
throw err;
|
|
1107
|
+
}
|
|
1108
|
+
if (typeof engine.stop === 'function') {
|
|
1109
|
+
try { await engine.stop(); } catch (e) {}
|
|
1110
|
+
}
|
|
1111
|
+
this._databases.delete(name);
|
|
1112
|
+
const dir = this._dbPath(name);
|
|
1113
|
+
if (dir && fs.existsSync(dir)) {
|
|
1114
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
_userDbList(user) {
|
|
1119
|
+
if (!this.auth) return null;
|
|
1120
|
+
const entry = this.auth[user];
|
|
1121
|
+
if (entry && typeof entry === 'object') {
|
|
1122
|
+
if (Array.isArray(entry.databases)) return entry.databases;
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
return [];
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
_canAccessDb(user, dbName) {
|
|
1129
|
+
const list = this._userDbList(user);
|
|
1130
|
+
if (list === null) return true;
|
|
1131
|
+
return list.includes(dbName);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
async _switchDb(conn, dbName) {
|
|
1135
|
+
if (!this._canAccessDb(conn.user, dbName)) {
|
|
1136
|
+
const err = new Error(`Access denied for user '${conn.user}' to database '${dbName}'`);
|
|
1137
|
+
err.code = 1044;
|
|
1138
|
+
throw err;
|
|
1139
|
+
}
|
|
1140
|
+
await this._getDatabase(dbName);
|
|
1141
|
+
conn.currentDb = dbName;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
_checkAuth(user, authResponse, seed) {
|
|
1145
|
+
if (this.auth) {
|
|
1146
|
+
if (!Object.prototype.hasOwnProperty.call(this.auth, user)) return false;
|
|
1147
|
+
const entry = this.auth[user];
|
|
1148
|
+
const pwd = entry && typeof entry === 'object' ? entry.password : entry;
|
|
1149
|
+
if (!pwd) return authResponse.length === 0;
|
|
1150
|
+
const pwdHash1 = crypto.createHash('sha1').update(pwd).digest();
|
|
1151
|
+
const pwdHash2 = crypto.createHash('sha1').update(pwdHash1).digest();
|
|
1152
|
+
const seedHash = crypto.createHash('sha1').update(Buffer.concat([seed, pwdHash2])).digest();
|
|
1153
|
+
const expected = Buffer.alloc(20);
|
|
1154
|
+
for (let i = 0; i < 20; i++) expected[i] = pwdHash1[i] ^ seedHash[i];
|
|
1155
|
+
return authResponse.length === 20 && crypto.timingSafeEqual(expected, authResponse);
|
|
1156
|
+
}
|
|
1157
|
+
if (this.user === null) {
|
|
1158
|
+
// 未配置认证:默认拒绝所有登录(防配置遗漏导致认证旁路)。
|
|
1159
|
+
// 仅当显式设置 noAuth: true 时允许无认证连接(仅限本地开发)。
|
|
1160
|
+
return this.options.noAuth === true;
|
|
1161
|
+
}
|
|
1162
|
+
if (user !== this.user) return false;
|
|
1163
|
+
if (!this.password) return authResponse.length === 0;
|
|
1164
|
+
const pwdHash1 = crypto.createHash('sha1').update(this.password).digest();
|
|
1165
|
+
const pwdHash2 = crypto.createHash('sha1').update(pwdHash1).digest();
|
|
1166
|
+
const seedHash = crypto.createHash('sha1').update(Buffer.concat([seed, pwdHash2])).digest();
|
|
1167
|
+
const expected = Buffer.alloc(20);
|
|
1168
|
+
for (let i = 0; i < 20; i++) expected[i] = pwdHash1[i] ^ seedHash[i];
|
|
1169
|
+
return authResponse.length === 20 && crypto.timingSafeEqual(expected, authResponse);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
listen(cb) {
|
|
1173
|
+
const ready = this._dbDir ? Promise.resolve() : this._getEngine();
|
|
1174
|
+
ready.then(() => {
|
|
1175
|
+
this._server = net.createServer(socket => {
|
|
1176
|
+
if (this._sockets.size >= this.maxConnections) {
|
|
1177
|
+
this._onSecurityEvent({ type: 'max-connections', remote: socket.remoteAddress });
|
|
1178
|
+
socket.destroy();
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
this._sockets.add(socket);
|
|
1182
|
+
socket.on('close', () => this._sockets.delete(socket));
|
|
1183
|
+
new MysqlConnection(socket, this);
|
|
1184
|
+
});
|
|
1185
|
+
this._server.once('error', err => {
|
|
1186
|
+
if (err && err.code === 'EADDRINUSE') {
|
|
1187
|
+
const friendly = new Error(
|
|
1188
|
+
`MysqlServer 端口 ${this.port} 已被占用(EADDRINUSE)。` +
|
|
1189
|
+
`请换一个空闲端口,或传 port: 0 让系统分配临时端口。`
|
|
1190
|
+
);
|
|
1191
|
+
friendly.code = 'EADDRINUSE';
|
|
1192
|
+
friendly.cause = err;
|
|
1193
|
+
if (cb) cb(friendly);
|
|
1194
|
+
else throw friendly;
|
|
1195
|
+
} else if (cb) cb(err);
|
|
1196
|
+
});
|
|
1197
|
+
this._server.listen(this.port, this.host, cb || (() => {}));
|
|
1198
|
+
}).catch(err => {
|
|
1199
|
+
if (cb) cb(err);
|
|
1200
|
+
else throw err;
|
|
1201
|
+
});
|
|
1202
|
+
return this;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
get address() {
|
|
1206
|
+
return this._server ? this._server.address() : null;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
close(cb) {
|
|
1210
|
+
const done = () => {
|
|
1211
|
+
for (const s of this._sockets) s.destroy();
|
|
1212
|
+
const stops = [];
|
|
1213
|
+
if (this._ownEngine && this._engine && typeof this._engine.stop === 'function') {
|
|
1214
|
+
stops.push(this._engine.stop());
|
|
1215
|
+
}
|
|
1216
|
+
for (const engine of this._databases.values()) {
|
|
1217
|
+
if (engine && typeof engine.stop === 'function') stops.push(engine.stop());
|
|
1218
|
+
}
|
|
1219
|
+
this._databases.clear();
|
|
1220
|
+
if (stops.length > 0) {
|
|
1221
|
+
Promise.allSettled(stops).then(() => cb && cb());
|
|
1222
|
+
} else if (cb) cb();
|
|
1223
|
+
};
|
|
1224
|
+
if (this._server) {
|
|
1225
|
+
this._server.close(() => done());
|
|
1226
|
+
} else {
|
|
1227
|
+
done();
|
|
1228
|
+
}
|
|
1229
|
+
return this;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function createMysqlServer(options) {
|
|
1234
|
+
return new MysqlServer(options || {});
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
module.exports = { createMysqlServer, MysqlServer, MysqlConnection };
|