@bumail/smtp 0.4.0 → 0.6.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/README.md +64 -5
- package/dist/chunks/{index-753q702a.js → index-n4xwep5m.js} +3 -3
- package/dist/chunks/{index-753q702a.js.map → index-n4xwep5m.js.map} +3 -3
- package/dist/client/index.js +1 -1
- package/dist/errors.d.ts +3 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +617 -71
- package/dist/index.js.map +15 -8
- package/dist/server/admission.d.ts +37 -0
- package/dist/server/admission.d.ts.map +1 -0
- package/dist/server/listener.d.ts +17 -8
- package/dist/server/listener.d.ts.map +1 -1
- package/dist/server/options.d.ts +21 -1
- package/dist/server/options.d.ts.map +1 -1
- package/dist/server/proxy/address.d.ts +17 -0
- package/dist/server/proxy/address.d.ts.map +1 -0
- package/dist/server/proxy/header.d.ts +26 -0
- package/dist/server/proxy/header.d.ts.map +1 -0
- package/dist/server/proxy/reader.d.ts +15 -0
- package/dist/server/proxy/reader.d.ts.map +1 -0
- package/dist/server/proxy/tls.d.ts +50 -0
- package/dist/server/proxy/tls.d.ts.map +1 -0
- package/dist/server/proxy/trusted.d.ts +9 -0
- package/dist/server/proxy/trusted.d.ts.map +1 -0
- package/dist/server/raw-socket.d.ts +9 -0
- package/dist/server/raw-socket.d.ts.map +1 -0
- package/dist/server/received.d.ts +2 -0
- package/dist/server/received.d.ts.map +1 -1
- package/dist/server/server.d.ts +9 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/settings.d.ts +5 -0
- package/dist/server/settings.d.ts.map +1 -1
- package/dist/server/tls-context.d.ts +46 -0
- package/dist/server/tls-context.d.ts.map +1 -0
- package/dist/server/transport.d.ts +2 -2
- package/dist/server/transport.d.ts.map +1 -1
- package/docs/README.md +2 -2
- package/docs/guide.md +417 -13
- package/docs/roadmap.md +19 -0
- package/docs/troubleshooting.md +368 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
decodeLoginStep2,
|
|
8
8
|
LineSplitter,
|
|
9
9
|
Outgoing
|
|
10
|
-
} from "./chunks/index-
|
|
10
|
+
} from "./chunks/index-n4xwep5m.js";
|
|
11
11
|
// src/protocol/command.ts
|
|
12
12
|
function parseCommand(line) {
|
|
13
13
|
const space = line.indexOf(" ");
|
|
@@ -474,6 +474,7 @@ async function runCommand(connection, line) {
|
|
|
474
474
|
}
|
|
475
475
|
|
|
476
476
|
// src/server/received.ts
|
|
477
|
+
import { isIPv6 } from "node:net";
|
|
477
478
|
var DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
478
479
|
var MONTHS = [
|
|
479
480
|
"Jan",
|
|
@@ -498,6 +499,10 @@ function protocolOf(esmtp, secure, authenticated) {
|
|
|
498
499
|
return "SMTP";
|
|
499
500
|
return `ESMTP${secure ? "S" : ""}${authenticated ? "A" : ""}`;
|
|
500
501
|
}
|
|
502
|
+
function addressLiteral(address) {
|
|
503
|
+
const bare = address.replace(/%.*$/, "");
|
|
504
|
+
return isIPv6(bare) ? `IPv6:${bare}` : printable(address);
|
|
505
|
+
}
|
|
501
506
|
function printable(text) {
|
|
502
507
|
let out = "";
|
|
503
508
|
for (const char of text) {
|
|
@@ -512,7 +517,7 @@ function plain(address) {
|
|
|
512
517
|
function receivedField(connection, recipients, id, now = new Date) {
|
|
513
518
|
const { state, transport, settings } = connection;
|
|
514
519
|
const lines = [
|
|
515
|
-
`Received: from ${printable(state.helo ?? "unknown")} ([${
|
|
520
|
+
`Received: from ${printable(state.helo ?? "unknown")} ([${addressLiteral(transport.remoteAddress)}])`,
|
|
516
521
|
` by ${settings.options.hostname} with ${protocolOf(state.esmtp, state.secure, state.user !== undefined)} id ${id}`
|
|
517
522
|
];
|
|
518
523
|
const [recipient] = recipients;
|
|
@@ -1124,19 +1129,33 @@ class SocketTransport {
|
|
|
1124
1129
|
}
|
|
1125
1130
|
|
|
1126
1131
|
// src/server/listener.ts
|
|
1132
|
+
function received(socket, settings, chunk) {
|
|
1133
|
+
socket.data.transport?.received();
|
|
1134
|
+
socket.timeout(settings.timeout);
|
|
1135
|
+
socket.data.connection?.receive(chunk);
|
|
1136
|
+
}
|
|
1127
1137
|
function handlers(settings) {
|
|
1128
1138
|
const idle = reply(421, "4.4.2", `${settings.options.hostname} Idle too long, closing`);
|
|
1129
1139
|
return {
|
|
1130
1140
|
data(socket, chunk) {
|
|
1131
|
-
|
|
1141
|
+
const state = socket.data;
|
|
1142
|
+
if (!state || state.upgraded)
|
|
1132
1143
|
return;
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1144
|
+
if (state.header)
|
|
1145
|
+
return state.header.receive(chunk);
|
|
1146
|
+
if (!state.tls)
|
|
1147
|
+
return received(socket, settings, chunk);
|
|
1148
|
+
state.transport?.received();
|
|
1149
|
+
state.tls.receive(chunk);
|
|
1136
1150
|
},
|
|
1137
1151
|
drain(socket) {
|
|
1138
|
-
|
|
1139
|
-
|
|
1152
|
+
const state = socket.data;
|
|
1153
|
+
if (!state || state.upgraded)
|
|
1154
|
+
return;
|
|
1155
|
+
if (state.tls)
|
|
1156
|
+
state.tls.rawDrain();
|
|
1157
|
+
else
|
|
1158
|
+
state.transport?.drain();
|
|
1140
1159
|
},
|
|
1141
1160
|
error(socket) {
|
|
1142
1161
|
socket.data?.connection?.close();
|
|
@@ -1154,15 +1173,15 @@ function handlers(settings) {
|
|
|
1154
1173
|
}
|
|
1155
1174
|
};
|
|
1156
1175
|
}
|
|
1157
|
-
function turnAway(socket, secure, line) {
|
|
1158
|
-
const refused = new SocketTransport(
|
|
1176
|
+
function turnAway(socket, raw, secure, line) {
|
|
1177
|
+
const refused = new SocketTransport(raw, secure, () => {});
|
|
1159
1178
|
socket.data.transport = refused;
|
|
1160
1179
|
refused.write(line);
|
|
1161
1180
|
refused.end();
|
|
1162
1181
|
}
|
|
1163
|
-
function transportOf(socket, settings, secure, slots) {
|
|
1164
|
-
const transport = new SocketTransport(
|
|
1165
|
-
const tls = settings.options
|
|
1182
|
+
function transportOf(socket, raw, address, settings, secure, slots) {
|
|
1183
|
+
const transport = new SocketTransport(raw, secure, () => {
|
|
1184
|
+
const tls = settings.tls?.options;
|
|
1166
1185
|
const connection = socket.data.connection;
|
|
1167
1186
|
if (!tls || !connection)
|
|
1168
1187
|
return;
|
|
@@ -1180,53 +1199,341 @@ function transportOf(socket, settings, secure, slots) {
|
|
|
1180
1199
|
}
|
|
1181
1200
|
});
|
|
1182
1201
|
encrypted.timeout(settings.timeout);
|
|
1183
|
-
const next = new SocketTransport(encrypted, true, () => {},
|
|
1202
|
+
const next = new SocketTransport(encrypted, true, () => {}, address);
|
|
1184
1203
|
encrypted.data.transport = next;
|
|
1185
1204
|
connection.useTransport(next);
|
|
1186
|
-
});
|
|
1205
|
+
}, address);
|
|
1187
1206
|
socket.data.transport = transport;
|
|
1188
1207
|
return transport;
|
|
1189
1208
|
}
|
|
1190
|
-
|
|
1209
|
+
|
|
1210
|
+
// src/server/proxy/address.ts
|
|
1211
|
+
import { isIP as isIP2 } from "node:net";
|
|
1212
|
+
function addressBytes(text) {
|
|
1213
|
+
if (text.includes("%"))
|
|
1214
|
+
return;
|
|
1215
|
+
const kind = isIP2(text);
|
|
1216
|
+
if (kind === 4)
|
|
1217
|
+
return new Uint8Array(text.split(".").map(Number));
|
|
1218
|
+
if (kind !== 6)
|
|
1219
|
+
return;
|
|
1220
|
+
return unmapped(ipv6Bytes(text.toLowerCase()));
|
|
1221
|
+
}
|
|
1222
|
+
function ipv6Bytes(text) {
|
|
1223
|
+
const bytes = new Uint8Array(16);
|
|
1224
|
+
let tail = text;
|
|
1225
|
+
const dotted = /(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(text);
|
|
1226
|
+
if (dotted) {
|
|
1227
|
+
bytes.set(dotted.slice(1).map(Number), 12);
|
|
1228
|
+
tail = `${text.slice(0, dotted.index)}0:0`;
|
|
1229
|
+
}
|
|
1230
|
+
const [head = "", rest] = tail.split("::");
|
|
1231
|
+
const left = head === "" ? [] : head.split(":");
|
|
1232
|
+
const right = rest === undefined || rest === "" ? [] : rest.split(":");
|
|
1233
|
+
const groups = rest === undefined ? left : [...left, ...Array(8 - left.length - right.length).fill("0"), ...right];
|
|
1234
|
+
const view = new DataView(bytes.buffer);
|
|
1235
|
+
groups.forEach((group, at) => {
|
|
1236
|
+
if (!dotted || at < 6)
|
|
1237
|
+
view.setUint16(at * 2, Number.parseInt(group, 16));
|
|
1238
|
+
});
|
|
1239
|
+
return bytes;
|
|
1240
|
+
}
|
|
1241
|
+
function unmapped(bytes) {
|
|
1242
|
+
if (bytes.length !== 16)
|
|
1243
|
+
return bytes;
|
|
1244
|
+
const mapped = bytes.subarray(0, 10).every((byte) => byte === 0) && bytes[10] === 255 && bytes[11] === 255;
|
|
1245
|
+
return mapped ? bytes.slice(12) : bytes;
|
|
1246
|
+
}
|
|
1247
|
+
function formatAddress(raw) {
|
|
1248
|
+
const bytes = unmapped(raw);
|
|
1249
|
+
if (bytes.length === 4)
|
|
1250
|
+
return bytes.join(".");
|
|
1251
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, 16);
|
|
1252
|
+
const groups = Array.from({ length: 8 }, (_, at) => view.getUint16(at * 2));
|
|
1253
|
+
let best = { at: -1, length: 1 };
|
|
1254
|
+
for (let at = 0;at < 8; ) {
|
|
1255
|
+
let end = at;
|
|
1256
|
+
while (end < 8 && groups[end] === 0)
|
|
1257
|
+
end++;
|
|
1258
|
+
if (end - at > best.length)
|
|
1259
|
+
best = { at, length: end - at };
|
|
1260
|
+
at = end === at ? at + 1 : end;
|
|
1261
|
+
}
|
|
1262
|
+
const hex = groups.map((group) => group.toString(16));
|
|
1263
|
+
if (best.at < 0)
|
|
1264
|
+
return hex.join(":");
|
|
1265
|
+
const left = hex.slice(0, best.at).join(":");
|
|
1266
|
+
const right = hex.slice(best.at + best.length).join(":");
|
|
1267
|
+
return `${left}::${right}`;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// src/server/proxy/header.ts
|
|
1271
|
+
var MAX_V1_LENGTH = 107;
|
|
1272
|
+
var MAX_TLV_BYTES = 2048;
|
|
1273
|
+
var MAX_HEADER_LENGTH = 16 + 216 + MAX_TLV_BYTES;
|
|
1274
|
+
var V1_PREFIX = new TextEncoder().encode("PROXY ");
|
|
1275
|
+
var V2_SIGNATURE = new Uint8Array([
|
|
1276
|
+
13,
|
|
1277
|
+
10,
|
|
1278
|
+
13,
|
|
1279
|
+
10,
|
|
1280
|
+
0,
|
|
1281
|
+
13,
|
|
1282
|
+
10,
|
|
1283
|
+
81,
|
|
1284
|
+
85,
|
|
1285
|
+
73,
|
|
1286
|
+
84,
|
|
1287
|
+
10
|
|
1288
|
+
]);
|
|
1289
|
+
var ADDRESS_LENGTH = [0, 12, 36, 216];
|
|
1290
|
+
var PARTIAL = { status: "partial" };
|
|
1291
|
+
var invalid = (reason) => ({ status: "invalid", reason });
|
|
1292
|
+
function startsAs(bytes, prefix) {
|
|
1293
|
+
const length = Math.min(bytes.length, prefix.length);
|
|
1294
|
+
for (let at = 0;at < length; at++) {
|
|
1295
|
+
if (bytes[at] !== prefix[at])
|
|
1296
|
+
return false;
|
|
1297
|
+
}
|
|
1298
|
+
return true;
|
|
1299
|
+
}
|
|
1300
|
+
function readProxyHeader(bytes) {
|
|
1301
|
+
if (bytes.length === 0)
|
|
1302
|
+
return PARTIAL;
|
|
1303
|
+
if (startsAs(bytes, V2_SIGNATURE))
|
|
1304
|
+
return readV2(bytes);
|
|
1305
|
+
if (startsAs(bytes, V1_PREFIX))
|
|
1306
|
+
return readV1(bytes);
|
|
1307
|
+
return invalid("no PROXY protocol signature");
|
|
1308
|
+
}
|
|
1309
|
+
function readV2(bytes) {
|
|
1310
|
+
if (bytes.length < 16)
|
|
1311
|
+
return PARTIAL;
|
|
1312
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1313
|
+
const versionCommand = view.getUint8(12);
|
|
1314
|
+
if (versionCommand >> 4 !== 2)
|
|
1315
|
+
return invalid("not version 2");
|
|
1316
|
+
const command = versionCommand & 15;
|
|
1317
|
+
if (command > 1)
|
|
1318
|
+
return invalid(`unknown command ${command}`);
|
|
1319
|
+
const family = view.getUint8(13) >> 4;
|
|
1320
|
+
const transport = view.getUint8(13) & 15;
|
|
1321
|
+
const length = view.getUint16(14);
|
|
1322
|
+
const addresses = command === 0 ? 0 : ADDRESS_LENGTH[family];
|
|
1323
|
+
if (addresses === undefined || transport > 2) {
|
|
1324
|
+
return invalid("unknown address family or transport");
|
|
1325
|
+
}
|
|
1326
|
+
if (length < addresses)
|
|
1327
|
+
return invalid("address block too short");
|
|
1328
|
+
if (length - addresses > MAX_TLV_BYTES)
|
|
1329
|
+
return invalid("TLVs too long");
|
|
1330
|
+
const total = 16 + length;
|
|
1331
|
+
if (bytes.length < total)
|
|
1332
|
+
return PARTIAL;
|
|
1333
|
+
if (command === 0)
|
|
1334
|
+
return { status: "complete", length: total };
|
|
1335
|
+
const tlvs = bytes.subarray(16 + addresses, total);
|
|
1336
|
+
if (!wellFormedTlvs(tlvs))
|
|
1337
|
+
return invalid("malformed TLV");
|
|
1338
|
+
if (transport !== 1 || family !== 1 && family !== 2) {
|
|
1339
|
+
return { status: "complete", length: total };
|
|
1340
|
+
}
|
|
1341
|
+
const size = family === 1 ? 4 : 16;
|
|
1342
|
+
const source = formatAddress(bytes.slice(16, 16 + size));
|
|
1343
|
+
return { status: "complete", length: total, source };
|
|
1344
|
+
}
|
|
1345
|
+
function wellFormedTlvs(block) {
|
|
1346
|
+
let at = 0;
|
|
1347
|
+
while (at < block.length) {
|
|
1348
|
+
if (at + 3 > block.length)
|
|
1349
|
+
return false;
|
|
1350
|
+
const length = block[at + 1] << 8 | block[at + 2];
|
|
1351
|
+
at += 3 + length;
|
|
1352
|
+
}
|
|
1353
|
+
return at === block.length;
|
|
1354
|
+
}
|
|
1355
|
+
var PORT = /^(0|[1-9]\d{0,4})$/;
|
|
1356
|
+
function readV1(bytes) {
|
|
1357
|
+
const end = bytes.indexOf(10);
|
|
1358
|
+
if (end < 0) {
|
|
1359
|
+
return bytes.length >= MAX_V1_LENGTH ? invalid("v1 line too long") : PARTIAL;
|
|
1360
|
+
}
|
|
1361
|
+
if (end + 1 > MAX_V1_LENGTH)
|
|
1362
|
+
return invalid("v1 line too long");
|
|
1363
|
+
if (bytes[end - 1] !== 13)
|
|
1364
|
+
return invalid("v1 line without CRLF");
|
|
1365
|
+
const line = bytes.subarray(0, end - 1);
|
|
1366
|
+
if (line.some((byte) => byte < 32 || byte > 126)) {
|
|
1367
|
+
return invalid("v1 line not printable");
|
|
1368
|
+
}
|
|
1369
|
+
const [, protocol, ...rest] = new TextDecoder().decode(line).split(" ");
|
|
1370
|
+
if (protocol === "UNKNOWN")
|
|
1371
|
+
return { status: "complete", length: end + 1 };
|
|
1372
|
+
const [source = "", destination = "", sourcePort = "", port = ""] = rest;
|
|
1373
|
+
const size = protocol === "TCP4" ? 4 : protocol === "TCP6" ? 16 : 0;
|
|
1374
|
+
if (size === 0)
|
|
1375
|
+
return invalid("v1 protocol not TCP4, TCP6 or UNKNOWN");
|
|
1376
|
+
const from = rawAddress(source, size);
|
|
1377
|
+
if (rest.length !== 4 || !from || !rawAddress(destination, size) || !validPort(sourcePort) || !validPort(port)) {
|
|
1378
|
+
return invalid("v1 addresses or ports malformed");
|
|
1379
|
+
}
|
|
1380
|
+
return { status: "complete", length: end + 1, source: formatAddress(from) };
|
|
1381
|
+
}
|
|
1382
|
+
function rawAddress(text, size) {
|
|
1383
|
+
if (size === 4 && text.includes(":"))
|
|
1384
|
+
return;
|
|
1385
|
+
if (size === 16 && !text.includes(":"))
|
|
1386
|
+
return;
|
|
1387
|
+
return addressBytes(text);
|
|
1388
|
+
}
|
|
1389
|
+
function validPort(text) {
|
|
1390
|
+
return PORT.test(text) && Number(text) <= 65535;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// src/server/proxy/reader.ts
|
|
1394
|
+
class HeaderReader {
|
|
1395
|
+
#bytes = new Uint8Array(0);
|
|
1396
|
+
#timer;
|
|
1397
|
+
#settled = false;
|
|
1398
|
+
#done;
|
|
1399
|
+
#refuse;
|
|
1400
|
+
constructor(seconds, done, refuse) {
|
|
1401
|
+
this.#done = done;
|
|
1402
|
+
this.#refuse = refuse;
|
|
1403
|
+
this.#timer = setTimeout(() => this.#settle(refuse), seconds * 1000);
|
|
1404
|
+
this.#timer.unref?.();
|
|
1405
|
+
}
|
|
1406
|
+
receive(chunk) {
|
|
1407
|
+
if (this.#settled)
|
|
1408
|
+
return;
|
|
1409
|
+
const bytes = new Uint8Array(this.#bytes.length + chunk.length);
|
|
1410
|
+
bytes.set(this.#bytes);
|
|
1411
|
+
bytes.set(chunk, this.#bytes.length);
|
|
1412
|
+
this.#bytes = bytes;
|
|
1413
|
+
const header = readProxyHeader(bytes);
|
|
1414
|
+
if (header.status === "complete") {
|
|
1415
|
+
const rest = bytes.slice(header.length);
|
|
1416
|
+
this.#settle(() => this.#done(header.source, rest));
|
|
1417
|
+
} else if (header.status === "invalid" || bytes.length >= MAX_HEADER_LENGTH) {
|
|
1418
|
+
this.#settle(this.#refuse);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
cancel() {
|
|
1422
|
+
this.#settled = true;
|
|
1423
|
+
clearTimeout(this.#timer);
|
|
1424
|
+
this.#bytes = new Uint8Array(0);
|
|
1425
|
+
}
|
|
1426
|
+
#settle(then) {
|
|
1427
|
+
if (this.#settled)
|
|
1428
|
+
return;
|
|
1429
|
+
this.cancel();
|
|
1430
|
+
then();
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// src/server/proxy/tls.ts
|
|
1435
|
+
import { Duplex } from "node:stream";
|
|
1436
|
+
import { TLSSocket } from "node:tls";
|
|
1437
|
+
|
|
1438
|
+
class ProxiedTls {
|
|
1439
|
+
#raw;
|
|
1440
|
+
#wire;
|
|
1441
|
+
#tls;
|
|
1442
|
+
#pending;
|
|
1443
|
+
#halfClose = true;
|
|
1444
|
+
constructor(raw, context, events) {
|
|
1445
|
+
this.#raw = raw;
|
|
1446
|
+
this.#wire = new Duplex({
|
|
1447
|
+
read() {},
|
|
1448
|
+
write: (chunk, _encoding, done) => this.#send(chunk, done),
|
|
1449
|
+
final: (done) => {
|
|
1450
|
+
this.#raw.shutdown(this.#halfClose);
|
|
1451
|
+
done();
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
this.#tls = new TLSSocket(this.#wire, {
|
|
1455
|
+
isServer: true,
|
|
1456
|
+
secureContext: context
|
|
1457
|
+
});
|
|
1458
|
+
this.#tls.on("secure", () => events.secure());
|
|
1459
|
+
this.#tls.on("data", (bytes) => events.data(bytes));
|
|
1460
|
+
this.#tls.on("drain", () => events.drain());
|
|
1461
|
+
this.#tls.on("error", () => this.terminate());
|
|
1462
|
+
}
|
|
1463
|
+
get remoteAddress() {
|
|
1464
|
+
return this.#raw.remoteAddress;
|
|
1465
|
+
}
|
|
1466
|
+
receive(chunk) {
|
|
1467
|
+
this.#wire.push(Uint8Array.from(chunk));
|
|
1468
|
+
}
|
|
1469
|
+
rawDrain() {
|
|
1470
|
+
const pending = this.#pending;
|
|
1471
|
+
if (!pending)
|
|
1472
|
+
return;
|
|
1473
|
+
const written = Math.max(this.#raw.write(pending.bytes), 0);
|
|
1474
|
+
if (written < pending.bytes.length) {
|
|
1475
|
+
pending.bytes = pending.bytes.subarray(written);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
this.#pending = undefined;
|
|
1479
|
+
pending.done();
|
|
1480
|
+
}
|
|
1481
|
+
#send(chunk, done) {
|
|
1482
|
+
const written = Math.max(this.#raw.write(chunk), 0);
|
|
1483
|
+
if (written < chunk.length) {
|
|
1484
|
+
this.#pending = { bytes: chunk.subarray(written), done };
|
|
1485
|
+
} else
|
|
1486
|
+
done();
|
|
1487
|
+
}
|
|
1488
|
+
write(bytes) {
|
|
1489
|
+
if (this.#tls.destroyed || this.#tls.writableNeedDrain)
|
|
1490
|
+
return 0;
|
|
1491
|
+
this.#tls.write(bytes);
|
|
1492
|
+
return bytes.length;
|
|
1493
|
+
}
|
|
1494
|
+
shutdown(halfClose = false) {
|
|
1495
|
+
this.#halfClose = halfClose;
|
|
1496
|
+
this.#tls.once("finish", () => this.#wire.end());
|
|
1497
|
+
this.#tls.end();
|
|
1498
|
+
}
|
|
1499
|
+
terminate() {
|
|
1500
|
+
this.#pending = undefined;
|
|
1501
|
+
this.#tls.destroy();
|
|
1502
|
+
this.#raw.terminate();
|
|
1503
|
+
}
|
|
1504
|
+
closed() {
|
|
1505
|
+
this.#pending = undefined;
|
|
1506
|
+
this.#tls.destroy();
|
|
1507
|
+
}
|
|
1508
|
+
pause() {
|
|
1509
|
+
this.#raw.pause();
|
|
1510
|
+
}
|
|
1511
|
+
resume() {
|
|
1512
|
+
this.#raw.resume();
|
|
1513
|
+
}
|
|
1514
|
+
timeout(seconds) {
|
|
1515
|
+
this.#raw.timeout(seconds);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// src/server/admission.ts
|
|
1520
|
+
function listenerHandlers(listening) {
|
|
1521
|
+
const { settings, slots, handshaking, secure } = listening;
|
|
1191
1522
|
return {
|
|
1192
1523
|
open(socket) {
|
|
1193
1524
|
socket.data = { upgraded: false, handshaking: secure };
|
|
1194
1525
|
socket.timeout(secure ? settings.handshakeTimeout : settings.timeout);
|
|
1195
|
-
const
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
socket
|
|
1200
|
-
};
|
|
1201
|
-
if (slots.size >= settings.maxConnections) {
|
|
1202
|
-
return begin(() => turnAway(socket, secure, `421 4.3.2 ${settings.options.hostname} Too many connections, try later\r
|
|
1203
|
-
`));
|
|
1204
|
-
}
|
|
1205
|
-
const key = clientKey(socket.remoteAddress);
|
|
1206
|
-
if (slots.of(key) >= settings.maxConnectionsPerClient) {
|
|
1207
|
-
return begin(() => turnAway(socket, secure, `421 4.7.0 ${settings.options.hostname} Too many connections from your address, try later\r
|
|
1208
|
-
`));
|
|
1209
|
-
}
|
|
1210
|
-
const connection = new Connection(settings, transportOf(socket, settings, secure, slots));
|
|
1211
|
-
slots.take(connection, key);
|
|
1212
|
-
socket.data.connection = connection;
|
|
1213
|
-
begin(() => void connection.open());
|
|
1214
|
-
},
|
|
1215
|
-
handshake(socket, success) {
|
|
1216
|
-
const state = socket.data;
|
|
1217
|
-
if (!state?.handshaking)
|
|
1218
|
-
return;
|
|
1219
|
-
handshaking.delete(socket);
|
|
1220
|
-
if (!success)
|
|
1221
|
-
return;
|
|
1222
|
-
state.handshaking = false;
|
|
1223
|
-
socket.timeout(settings.timeout);
|
|
1224
|
-
const start = state.start;
|
|
1225
|
-
delete state.start;
|
|
1226
|
-
start?.();
|
|
1526
|
+
const peer = socket.remoteAddress;
|
|
1527
|
+
if (settings.trusts?.(peer))
|
|
1528
|
+
awaitHeader(listening, socket, peer);
|
|
1529
|
+
else
|
|
1530
|
+
admit(listening, socket, peer);
|
|
1227
1531
|
},
|
|
1532
|
+
handshake: (socket, success) => handshake(listening, socket, success),
|
|
1228
1533
|
close(socket) {
|
|
1229
1534
|
handshaking.delete(socket);
|
|
1535
|
+
socket.data?.header?.cancel();
|
|
1536
|
+
socket.data?.tls?.closed();
|
|
1230
1537
|
const connection = socket.data?.connection;
|
|
1231
1538
|
if (connection)
|
|
1232
1539
|
slots.release(connection);
|
|
@@ -1235,22 +1542,223 @@ function listenerHandlers(settings, slots, handshaking, secure) {
|
|
|
1235
1542
|
}
|
|
1236
1543
|
};
|
|
1237
1544
|
}
|
|
1545
|
+
function handshake({ settings, handshaking }, socket, success) {
|
|
1546
|
+
const state = socket.data;
|
|
1547
|
+
if (!state?.handshaking)
|
|
1548
|
+
return;
|
|
1549
|
+
handshaking.delete(socket);
|
|
1550
|
+
if (!success)
|
|
1551
|
+
return;
|
|
1552
|
+
state.handshaking = false;
|
|
1553
|
+
socket.timeout(settings.timeout);
|
|
1554
|
+
const start = state.start;
|
|
1555
|
+
delete state.start;
|
|
1556
|
+
start?.();
|
|
1557
|
+
}
|
|
1558
|
+
function awaitHeader(listening, socket, peer) {
|
|
1559
|
+
const { settings, handshaking, secure } = listening;
|
|
1560
|
+
socket.data.handshaking = true;
|
|
1561
|
+
socket.timeout(settings.handshakeTimeout);
|
|
1562
|
+
handshaking.add(socket);
|
|
1563
|
+
socket.data.header = new HeaderReader(settings.handshakeTimeout, (source, rest) => {
|
|
1564
|
+
delete socket.data.header;
|
|
1565
|
+
handshaking.delete(socket);
|
|
1566
|
+
socket.data.handshaking = secure;
|
|
1567
|
+
if (!secure)
|
|
1568
|
+
socket.timeout(settings.timeout);
|
|
1569
|
+
admit(listening, socket, source ?? peer, rest);
|
|
1570
|
+
}, () => socket.terminate());
|
|
1571
|
+
}
|
|
1572
|
+
function admit(listening, socket, address, first) {
|
|
1573
|
+
const { settings, slots, handshaking, secure, proxied } = listening;
|
|
1574
|
+
let raw = socket;
|
|
1575
|
+
if (proxied) {
|
|
1576
|
+
raw = startTls2(listening, socket, proxied);
|
|
1577
|
+
if (first && first.length > 0)
|
|
1578
|
+
socket.data.tls?.receive(first);
|
|
1579
|
+
}
|
|
1580
|
+
const begin = (start) => {
|
|
1581
|
+
if (!secure)
|
|
1582
|
+
return start();
|
|
1583
|
+
handshaking.add(socket);
|
|
1584
|
+
socket.data.start = start;
|
|
1585
|
+
};
|
|
1586
|
+
const { hostname } = settings.options;
|
|
1587
|
+
const refuse = (line) => begin(() => turnAway(socket, raw, secure, `${line}\r
|
|
1588
|
+
`));
|
|
1589
|
+
if (slots.size >= settings.maxConnections) {
|
|
1590
|
+
refuse(`421 4.3.2 ${hostname} Too many connections, try later`);
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
const key = clientKey(address);
|
|
1594
|
+
if (slots.of(key) >= settings.maxConnectionsPerClient) {
|
|
1595
|
+
refuse(`421 4.7.0 ${hostname} Too many connections from your address, try later`);
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
const transport = transportOf(socket, raw, address, settings, secure, slots);
|
|
1599
|
+
const connection = new Connection(settings, transport);
|
|
1600
|
+
slots.take(connection, key);
|
|
1601
|
+
socket.data.connection = connection;
|
|
1602
|
+
if (first && first.length > 0 && !proxied) {
|
|
1603
|
+
received(socket, settings, first);
|
|
1604
|
+
}
|
|
1605
|
+
begin(() => void connection.open());
|
|
1606
|
+
}
|
|
1607
|
+
function startTls2(listening, socket, tls) {
|
|
1608
|
+
const { settings } = listening;
|
|
1609
|
+
socket.data.handshaking = true;
|
|
1610
|
+
socket.timeout(settings.handshakeTimeout);
|
|
1611
|
+
const proxied = new ProxiedTls(socket, tls.context, {
|
|
1612
|
+
secure: () => handshake(listening, socket, true),
|
|
1613
|
+
data: (bytes) => received(socket, settings, bytes),
|
|
1614
|
+
drain: () => socket.data.transport?.drain()
|
|
1615
|
+
});
|
|
1616
|
+
socket.data.tls = proxied;
|
|
1617
|
+
return proxied;
|
|
1618
|
+
}
|
|
1619
|
+
function upgradingHandlers(encrypted, settings) {
|
|
1620
|
+
const ignore = () => {};
|
|
1621
|
+
return {
|
|
1622
|
+
open(raw) {
|
|
1623
|
+
const tls = settings.tls?.options;
|
|
1624
|
+
if (!tls)
|
|
1625
|
+
return raw.terminate();
|
|
1626
|
+
try {
|
|
1627
|
+
raw.upgradeTLS({
|
|
1628
|
+
tls: { key: tls.key, cert: tls.cert },
|
|
1629
|
+
data: { upgraded: false },
|
|
1630
|
+
socket: encrypted
|
|
1631
|
+
});
|
|
1632
|
+
} catch {
|
|
1633
|
+
raw.terminate();
|
|
1634
|
+
}
|
|
1635
|
+
},
|
|
1636
|
+
data: ignore,
|
|
1637
|
+
drain: ignore,
|
|
1638
|
+
close: ignore,
|
|
1639
|
+
error: ignore,
|
|
1640
|
+
timeout: ignore
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// src/server/proxy/trusted.ts
|
|
1645
|
+
function networkOf(entry) {
|
|
1646
|
+
if (typeof entry !== "string")
|
|
1647
|
+
return `${String(entry)} is not a string`;
|
|
1648
|
+
const [address = "", prefix, extra] = entry.split("/");
|
|
1649
|
+
const raw = addressBytes(address);
|
|
1650
|
+
if (!raw || extra !== undefined) {
|
|
1651
|
+
return `"${entry}" is neither an IP address nor a CIDR`;
|
|
1652
|
+
}
|
|
1653
|
+
const mapped = raw.length === 4 && address.includes(":");
|
|
1654
|
+
const width = raw.length * 8;
|
|
1655
|
+
if (prefix === undefined)
|
|
1656
|
+
return { bytes: raw, bits: width };
|
|
1657
|
+
let bits = /^\d{1,3}$/.test(prefix) ? Number(prefix) : -1;
|
|
1658
|
+
if (mapped)
|
|
1659
|
+
bits -= 96;
|
|
1660
|
+
if (bits < 0 || bits > width) {
|
|
1661
|
+
return `"${entry}" has a prefix length out of range`;
|
|
1662
|
+
}
|
|
1663
|
+
if (bits === 0) {
|
|
1664
|
+
return `"${entry}" has a prefix length of 0, which trusts every peer`;
|
|
1665
|
+
}
|
|
1666
|
+
return { bytes: raw, bits };
|
|
1667
|
+
}
|
|
1668
|
+
function contains(network, address) {
|
|
1669
|
+
if (network.bytes.length !== address.length)
|
|
1670
|
+
return false;
|
|
1671
|
+
const whole = network.bits >> 3;
|
|
1672
|
+
for (let at = 0;at < whole; at++) {
|
|
1673
|
+
if (network.bytes[at] !== address[at])
|
|
1674
|
+
return false;
|
|
1675
|
+
}
|
|
1676
|
+
const rest = network.bits & 7;
|
|
1677
|
+
if (rest === 0)
|
|
1678
|
+
return true;
|
|
1679
|
+
const mask = 255 << 8 - rest & 255;
|
|
1680
|
+
return (network.bytes[whole] & mask) === (address[whole] & mask);
|
|
1681
|
+
}
|
|
1682
|
+
function trustedPeers(entries, invalid) {
|
|
1683
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
1684
|
+
throw invalid("proxyProtocol.trusted must list the addresses or CIDRs of the proxies, at least one");
|
|
1685
|
+
}
|
|
1686
|
+
const networks = entries.map((entry) => {
|
|
1687
|
+
const network = networkOf(entry);
|
|
1688
|
+
if (typeof network === "string") {
|
|
1689
|
+
throw invalid(`proxyProtocol.trusted: ${network}`);
|
|
1690
|
+
}
|
|
1691
|
+
return network;
|
|
1692
|
+
});
|
|
1693
|
+
return (peer) => {
|
|
1694
|
+
const address = addressBytes(peer);
|
|
1695
|
+
return address !== undefined && networks.some((network) => contains(network, address));
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// src/server/tls-context.ts
|
|
1700
|
+
import { createSecureContext } from "node:tls";
|
|
1701
|
+
async function readTls(tls, fail, caller) {
|
|
1702
|
+
const read = async (value) => typeof value === "string" ? value : Buffer.from(value instanceof Uint8Array ? value : await value.bytes());
|
|
1703
|
+
try {
|
|
1704
|
+
const key = await read(tls.key);
|
|
1705
|
+
const cert = await read(tls.cert);
|
|
1706
|
+
if (key.length === 0 || cert.length === 0) {
|
|
1707
|
+
throw new Error(`the ${key.length === 0 ? "key" : "cert"} is empty`);
|
|
1708
|
+
}
|
|
1709
|
+
return { key, cert, context: createSecureContext({ key, cert }) };
|
|
1710
|
+
} catch (cause) {
|
|
1711
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
1712
|
+
throw fail(`${caller}: tls: { key, cert } cannot be used: ${reason}`, cause);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
class TlsHolder {
|
|
1717
|
+
#current;
|
|
1718
|
+
#tail = Promise.resolve();
|
|
1719
|
+
#given;
|
|
1720
|
+
constructor(given) {
|
|
1721
|
+
this.#given = given;
|
|
1722
|
+
}
|
|
1723
|
+
get options() {
|
|
1724
|
+
return this.#current ?? this.#given;
|
|
1725
|
+
}
|
|
1726
|
+
get context() {
|
|
1727
|
+
if (!this.#current)
|
|
1728
|
+
throw new Error("the TLS pair was never read");
|
|
1729
|
+
return this.#current.context;
|
|
1730
|
+
}
|
|
1731
|
+
async load(fail, caller) {
|
|
1732
|
+
if (this.#current)
|
|
1733
|
+
return this.#current.context;
|
|
1734
|
+
const read = await readTls(this.#given, fail, caller);
|
|
1735
|
+
this.#current ??= read;
|
|
1736
|
+
return this.#current.context;
|
|
1737
|
+
}
|
|
1738
|
+
replace(tls, fail, caller) {
|
|
1739
|
+
const run = this.#tail.then(async () => {
|
|
1740
|
+
this.#current = await readTls(tls, fail, caller);
|
|
1741
|
+
});
|
|
1742
|
+
this.#tail = run.catch(() => {});
|
|
1743
|
+
return run;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1238
1746
|
|
|
1239
1747
|
// src/server/settings.ts
|
|
1240
1748
|
var MAX_TIMER_SECONDS = Math.floor(2147483647 / 1000);
|
|
1241
|
-
var
|
|
1749
|
+
var invalid2 = (message) => new SmtpError2("INVALID_OPTION", `createSmtpServer(): ${message}`);
|
|
1242
1750
|
function positive(name, value, fallback) {
|
|
1243
1751
|
if (value === undefined)
|
|
1244
1752
|
return fallback;
|
|
1245
1753
|
if (!Number.isSafeInteger(value) || value < 1) {
|
|
1246
|
-
throw
|
|
1754
|
+
throw invalid2(`${name} must be a positive integer, not ${value}`);
|
|
1247
1755
|
}
|
|
1248
1756
|
return value;
|
|
1249
1757
|
}
|
|
1250
1758
|
function timer(name, value, fallback) {
|
|
1251
1759
|
const seconds = positive(name, value, fallback);
|
|
1252
1760
|
if (seconds > MAX_TIMER_SECONDS) {
|
|
1253
|
-
throw
|
|
1761
|
+
throw invalid2(`${name} must be at most ${MAX_TIMER_SECONDS} seconds, not ${seconds}`);
|
|
1254
1762
|
}
|
|
1255
1763
|
return seconds;
|
|
1256
1764
|
}
|
|
@@ -1258,7 +1766,7 @@ function nonNegative(name, value, fallback) {
|
|
|
1258
1766
|
if (value === undefined)
|
|
1259
1767
|
return fallback;
|
|
1260
1768
|
if (!Number.isFinite(value) || value < 0) {
|
|
1261
|
-
throw
|
|
1769
|
+
throw invalid2(`${name} must be a number of seconds, 0 or more, not ${value}`);
|
|
1262
1770
|
}
|
|
1263
1771
|
return value;
|
|
1264
1772
|
}
|
|
@@ -1269,32 +1777,33 @@ function localLookup(local) {
|
|
|
1269
1777
|
const domains = new Set(local.map((domain) => domain.toLowerCase()));
|
|
1270
1778
|
return (domain) => domains.has(domain);
|
|
1271
1779
|
}
|
|
1272
|
-
throw
|
|
1780
|
+
throw invalid2("localDomains must be an array of domains or a function");
|
|
1273
1781
|
}
|
|
1274
1782
|
function settingsOf(options) {
|
|
1275
1783
|
if (typeof options.hostname !== "string" || !/^[A-Za-z0-9.-]+$/.test(options.hostname)) {
|
|
1276
|
-
throw
|
|
1784
|
+
throw invalid2(`"${options.hostname}" is not a host name`);
|
|
1277
1785
|
}
|
|
1278
1786
|
if (typeof options.onData !== "function") {
|
|
1279
|
-
throw
|
|
1787
|
+
throw invalid2("onData must be a function: it is where messages go");
|
|
1280
1788
|
}
|
|
1281
1789
|
if (options.implicitTls && !options.tls) {
|
|
1282
|
-
throw
|
|
1790
|
+
throw invalid2("implicitTls needs tls: { key, cert }");
|
|
1283
1791
|
}
|
|
1284
1792
|
if (options.mode === "submission" && !options.authenticate) {
|
|
1285
|
-
throw
|
|
1793
|
+
throw invalid2("submission takes mail only from authenticated users, so it needs authenticate");
|
|
1286
1794
|
}
|
|
1287
1795
|
if (options.authenticate && !options.tls) {
|
|
1288
|
-
throw
|
|
1796
|
+
throw invalid2("authenticate needs tls: { key, cert }, since AUTH is offered only once encrypted");
|
|
1289
1797
|
}
|
|
1290
1798
|
const isLocal = localLookup(options.localDomains);
|
|
1291
1799
|
const timeout = positive("timeout", options.timeout, 300);
|
|
1292
1800
|
const greetingDelay = nonNegative("greetingDelay", options.greetingDelay, 0);
|
|
1293
1801
|
if (greetingDelay >= timeout) {
|
|
1294
|
-
throw
|
|
1802
|
+
throw invalid2(`greetingDelay (${greetingDelay} s) must be shorter than timeout (${timeout} s), or every client times out before the greeting`);
|
|
1295
1803
|
}
|
|
1296
1804
|
return {
|
|
1297
1805
|
options,
|
|
1806
|
+
...options.tls ? { tls: new TlsHolder(options.tls) } : {},
|
|
1298
1807
|
mode: options.mode ?? "mx",
|
|
1299
1808
|
maxMessageSize: positive("maxMessageSize", options.maxMessageSize, 25 * 1024 * 1024),
|
|
1300
1809
|
maxRecipients: positive("maxRecipients", options.maxRecipients, 100),
|
|
@@ -1305,6 +1814,7 @@ function settingsOf(options) {
|
|
|
1305
1814
|
hookTimeout: timer("hookTimeout", options.hookTimeout, 60),
|
|
1306
1815
|
handshakeTimeout: positive("handshakeTimeout", options.handshakeTimeout, 10),
|
|
1307
1816
|
greetingDelay,
|
|
1817
|
+
...options.proxyProtocol === undefined ? {} : { trusts: trustedPeers(options.proxyProtocol?.trusted, invalid2) },
|
|
1308
1818
|
isLocal: (domain) => isLocal(domain.toLowerCase())
|
|
1309
1819
|
};
|
|
1310
1820
|
}
|
|
@@ -1343,32 +1853,68 @@ class Slots {
|
|
|
1343
1853
|
}
|
|
1344
1854
|
|
|
1345
1855
|
// src/server/server.ts
|
|
1856
|
+
var invalidTls = (message, cause) => new SmtpError2("INVALID_OPTION", message, { cause });
|
|
1857
|
+
var STOPPED = "listen(): stop() was called before the server bound its port";
|
|
1858
|
+
async function bind(listening, port, hostname, stopped) {
|
|
1859
|
+
const { tls } = listening.settings;
|
|
1860
|
+
if (listening.secure && tls)
|
|
1861
|
+
await tls.load(invalidTls, "listen()");
|
|
1862
|
+
if (stopped()) {
|
|
1863
|
+
throw new SmtpError2("STOPPED", STOPPED);
|
|
1864
|
+
}
|
|
1865
|
+
const proxied = listening.secure && listening.settings.trusts ? tls : undefined;
|
|
1866
|
+
const full = {
|
|
1867
|
+
...handlers(listening.settings),
|
|
1868
|
+
...listenerHandlers({
|
|
1869
|
+
...listening,
|
|
1870
|
+
...proxied ? { proxied } : {}
|
|
1871
|
+
})
|
|
1872
|
+
};
|
|
1873
|
+
return Bun.listen({
|
|
1874
|
+
hostname,
|
|
1875
|
+
port,
|
|
1876
|
+
socket: listening.secure && !proxied ? upgradingHandlers(full, listening.settings) : full
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1346
1879
|
function createSmtpServer(options) {
|
|
1347
1880
|
const settings = settingsOf(options);
|
|
1348
1881
|
let listener;
|
|
1349
1882
|
const slots = new Slots;
|
|
1350
1883
|
const handshaking = new Set;
|
|
1351
1884
|
const secure = options.implicitTls === true;
|
|
1885
|
+
let starting = false;
|
|
1886
|
+
let stopRequested = false;
|
|
1352
1887
|
return {
|
|
1353
1888
|
get connections() {
|
|
1354
1889
|
return slots.size;
|
|
1355
1890
|
},
|
|
1891
|
+
async setTls(tls) {
|
|
1892
|
+
if (!settings.tls) {
|
|
1893
|
+
throw new SmtpError2("INVALID_OPTION", "setTls(): the server was made without tls, so it has none to replace");
|
|
1894
|
+
}
|
|
1895
|
+
await settings.tls.replace(tls, invalidTls, "setTls()");
|
|
1896
|
+
},
|
|
1356
1897
|
async listen({ port, hostname = "0.0.0.0" }) {
|
|
1357
|
-
if (listener) {
|
|
1358
|
-
throw new SmtpError2("ALREADY_LISTENING", `listen(): the server is already listening on ${listener.hostname}:${listener.port}`);
|
|
1898
|
+
if (listener || starting) {
|
|
1899
|
+
throw new SmtpError2("ALREADY_LISTENING", listener ? `listen(): the server is already listening on ${listener.hostname}:${listener.port}` : "listen(): the server is already starting to listen");
|
|
1359
1900
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1901
|
+
starting = true;
|
|
1902
|
+
stopRequested = false;
|
|
1903
|
+
try {
|
|
1904
|
+
const bound = await bind({ settings, slots, handshaking, secure }, port, hostname, () => stopRequested);
|
|
1905
|
+
if (stopRequested) {
|
|
1906
|
+
bound.stop(true);
|
|
1907
|
+
throw new SmtpError2("STOPPED", STOPPED);
|
|
1367
1908
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1909
|
+
listener = bound;
|
|
1910
|
+
return { port: listener.port, hostname: listener.hostname };
|
|
1911
|
+
} finally {
|
|
1912
|
+
starting = false;
|
|
1913
|
+
}
|
|
1370
1914
|
},
|
|
1371
1915
|
stop(closeConnections = false) {
|
|
1916
|
+
if (starting)
|
|
1917
|
+
stopRequested = true;
|
|
1372
1918
|
listener?.stop(closeConnections);
|
|
1373
1919
|
listener = undefined;
|
|
1374
1920
|
if (!closeConnections)
|
|
@@ -1400,5 +1946,5 @@ export {
|
|
|
1400
1946
|
reply
|
|
1401
1947
|
};
|
|
1402
1948
|
|
|
1403
|
-
//# debugId=
|
|
1949
|
+
//# debugId=2DE3A3E5E10EAC4A64756E2164756E21
|
|
1404
1950
|
//# sourceMappingURL=index.js.map
|