fix: honor Node all-address DNS lookup for HTTP webhooks
CSS quality / css-quality (push) Has been cancelled
CSS quality / css-quality (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
import { createServer, get, type RequestOptions } from 'node:http';
|
||||||
|
import type { AddressInfo } from 'node:net';
|
||||||
|
import { pinnedWebhookLookup } from './open-api.protocol';
|
||||||
|
|
||||||
|
describe('webhook pinned DNS lookup', () => {
|
||||||
|
it('supports single-address and all-address callbacks without resolving another address', () => {
|
||||||
|
const callback = jest.fn();
|
||||||
|
const lookup = pinnedWebhookLookup('203.0.113.10', 4);
|
||||||
|
lookup('ignored.example', {}, callback);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(null, '203.0.113.10', 4);
|
||||||
|
lookup('ignored.example', { all: true }, callback);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(null, [{ address: '203.0.113.10', family: 4 }]);
|
||||||
|
pinnedWebhookLookup('2001:db8::10', 6)('ignored.example', { all: true }, callback);
|
||||||
|
expect(callback).toHaveBeenLastCalledWith(null, [{ address: '2001:db8::10', family: 6 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('delivers through the real Node HTTP connector when automatic family selection requests all addresses', async () => {
|
||||||
|
const server = createServer((_request, response) => response.end('received'));
|
||||||
|
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||||
|
try {
|
||||||
|
const port = (server.address() as AddressInfo).port;
|
||||||
|
const options: RequestOptions & { autoSelectFamily: boolean } = {
|
||||||
|
lookup: pinnedWebhookLookup('127.0.0.1', 4),
|
||||||
|
autoSelectFamily: true,
|
||||||
|
agent: false,
|
||||||
|
};
|
||||||
|
const body = await new Promise<string>((resolve, reject) => {
|
||||||
|
const request = get(`http://webhook.invalid:${port}/`, options, (response) => {
|
||||||
|
let received = '';
|
||||||
|
response.setEncoding('utf8');
|
||||||
|
response.on('data', (chunk: string) => (received += chunk));
|
||||||
|
response.on('end', () => resolve(received));
|
||||||
|
});
|
||||||
|
request.setTimeout(3000, () => request.destroy(new Error('test HTTP timeout')));
|
||||||
|
request.on('error', reject);
|
||||||
|
});
|
||||||
|
expect(body).toBe('received');
|
||||||
|
} finally {
|
||||||
|
await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,14 @@
|
|||||||
import { createHash, createHmac } from 'node:crypto';
|
import { createHash, createHmac } from 'node:crypto';
|
||||||
import { HttpException } from '@nestjs/common';
|
import { HttpException } from '@nestjs/common';
|
||||||
|
import type { LookupFunction } from 'node:net';
|
||||||
|
|
||||||
|
/** Keep the validated address pinned while honoring Node's all-address lookup contract. */
|
||||||
|
export function pinnedWebhookLookup(address: string, family: number): LookupFunction {
|
||||||
|
return (_hostname, options, callback) => {
|
||||||
|
if (options.all) callback(null, [{ address, family }]);
|
||||||
|
else callback(null, address, family);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** v1 compatibility: an absent parsed body hashes as {}, never try alternate hashes. */
|
/** v1 compatibility: an absent parsed body hashes as {}, never try alternate hashes. */
|
||||||
export function openApiBodyHash(rawBody: Buffer | undefined, body: unknown) {
|
export function openApiBodyHash(rawBody: Buffer | undefined, body: unknown) {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { SendChainService } from '../send-chain/send-chain.service';
|
|||||||
import { decryptSecret, encryptSecret } from './open-api.crypto';
|
import { decryptSecret, encryptSecret } from './open-api.crypto';
|
||||||
import type { OpenApiAuthContext } from './open-api.types';
|
import type { OpenApiAuthContext } from './open-api.types';
|
||||||
import { ProtocolLogsService } from '../protocol-logs/protocol-logs.service';
|
import { ProtocolLogsService } from '../protocol-logs/protocol-logs.service';
|
||||||
import { publicOpenApiFailure, webhookJobId } from './open-api.protocol';
|
import { pinnedWebhookLookup, publicOpenApiFailure, webhookJobId } from './open-api.protocol';
|
||||||
import { automaticDeliveryMode } from './delivery-mode';
|
import { automaticDeliveryMode } from './delivery-mode';
|
||||||
|
|
||||||
export const OPEN_API_WEBHOOK_TRANSPORT = Symbol('open-api-webhook-transport');
|
export const OPEN_API_WEBHOOK_TRANSPORT = Symbol('open-api-webhook-transport');
|
||||||
@@ -934,7 +934,7 @@ async function postWebhook(
|
|||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { ...headers, 'content-length': String(Buffer.byteLength(body)) },
|
headers: { ...headers, 'content-length': String(Buffer.byteLength(body)) },
|
||||||
lookup: (_hostname, _options, callback) => callback(null, target.address, target.family),
|
lookup: pinnedWebhookLookup(target.address, target.family),
|
||||||
},
|
},
|
||||||
(response) => {
|
(response) => {
|
||||||
const chunks: Buffer[] = [];
|
const chunks: Buffer[] = [];
|
||||||
|
|||||||
@@ -2278,3 +2278,8 @@
|
|||||||
按[HTTP整改方案](http-api-assessment-20260910.md)本轮实施章节执行R01~R09。保持v1四接口和GET兼容摘要;补参数/响应契约、公共错误和交互关联ID。新请求以确定性业务关联及消息/响应/发送待办原子落库避免永久processing;未知结果必须核对,不自动新建短信。新回调耐久恢复采用安全任务编号、租约和原子尝试记录,旧回调不自动回填或重投。
|
按[HTTP整改方案](http-api-assessment-20260910.md)本轮实施章节执行R01~R09。保持v1四接口和GET兼容摘要;补参数/响应契约、公共错误和交互关联ID。新请求以确定性业务关联及消息/响应/发送待办原子落库避免永久processing;未知结果必须核对,不自动新建短信。新回调耐久恢复采用安全任务编号、租约和原子尝试记录,旧回调不自动回填或重投。
|
||||||
|
|
||||||
用户本轮明确:上行不得暴露通道、供应商编号及内部匹配诊断,仅输出客户自己的公共业务字段和所属企业/应用ID。公共文档与客户端Tab同源复用,未开通/无应用/配置失败可读通用文档,不能用默认值冒充真实应用配置。发送正向链路和历史回调处理须另有专项授权。本轮提交、推送、测试环境部署已授权;预生产未授权。
|
用户本轮明确:上行不得暴露通道、供应商编号及内部匹配诊断,仅输出客户自己的公共业务字段和所属企业/应用ID。公共文档与客户端Tab同源复用,未开通/无应用/配置失败可读通用文档,不能用默认值冒充真实应用配置。发送正向链路和历史回调处理须另有专项授权。本轮提交、推送、测试环境部署已授权;预生产未授权。
|
||||||
|
|
||||||
|
|
||||||
|
### 2026-09-14 HTTP回调传输兼容验收补充
|
||||||
|
|
||||||
|
Webhook需在当前受支持Node运行时通过真实HTTPS投递;SSRF校验后的固定DNS地址必须同时符合lookup单地址和all-address回调契约,不得通过重新解析或取消私网限制解决投递问题。完整模拟链路验收及限制见 [HTTP全量验收](http-api-full-acceptance-20260914.md)。
|
||||||
|
|||||||
@@ -286,3 +286,8 @@ R03不能仅通过“增加重试”关闭。后续详细设计至少说明:
|
|||||||
2026-09-14 核验:本地、真实 Git 远端、预生产部署标记均为 `d13ca0713abd6afbea5a62af39bcbb876b8bb186`。预生产鉴权 guard、OpenApiService、controller 经换行规范化的文本摘要与本地一致;公开 Swagger/JSON 均 HTTP 200。文档依据当前 controller、DTO、guard、service、异常过滤器、回执/上行事件生产代码和 Prisma 字段;未把历史数据库样本当作本次业务验收。
|
2026-09-14 核验:本地、真实 Git 远端、预生产部署标记均为 `d13ca0713abd6afbea5a62af39bcbb876b8bb186`。预生产鉴权 guard、OpenApiService、controller 经换行规范化的文本摘要与本地一致;公开 Swagger/JSON 均 HTTP 200。文档依据当前 controller、DTO、guard、service、异常过滤器、回执/上行事件生产代码和 Prisma 字段;未把历史数据库样本当作本次业务验收。
|
||||||
|
|
||||||
内部追踪:[测试用例](system-functional-test-cases.md)、[测试与实施进度](testing-progress.md)。后续实现发生变化时,签名函数、字段表、错误码、回调示例和在线契约须一起更新。
|
内部追踪:[测试用例](system-functional-test-cases.md)、[测试与实施进度](testing-progress.md)。后续实现发生变化时,签名函数、字段表、错误码、回调示例和在线契约须一起更新。
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-09-14 真实HTTP全量验收发现的Webhook DNS缺陷
|
||||||
|
|
||||||
|
测试环境 f0e8434 上,专用模拟应用已产生真实送达回执,但5个Webhook事件均在2次尝试后失败,lastError为 `Invalid IP address: undefined`,受控HTTPS接收端无请求。Node自动地址族选择以 `all=true` 调用自定义lookup,旧实现仍返回单地址三参数,违反回调契约。保留原SSRF解析、私网拒绝及地址固定,只按all选项返回固定地址数组或原单地址。未改变重试、计费、发送与租户规则。真实Node HTTPS连接旧实现失败/修正实现200,本地真实HTTP连接回归及API全量73套783项、类型构建通过;线上修复与Webhook全场景验收尚待标准发布完成。用户已另行授权本修复提交、推送及测试发布,不涉及预生产。完整结果将登记 [HTTP全量验收](http-api-full-acceptance-20260914.md)。
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# HTTP接口全量真实模拟验收(2026-09-14)
|
||||||
|
|
||||||
|
状态:执行中,不表示全部通过。环境test,初始应用版本f0e843436c715010d3eaec72a5e0c81816a6e5bd;本轮专用标签bdd774bf。
|
||||||
|
|
||||||
|
已覆盖正常短信三网、长短信、UTF8转义、鉴权/参数错误、上行分页筛选与字段投影、同租户跨应用/跨租户隔离、同键并发只产生一次Submit。测试证据位于 `%TEMP%/cmpp-http-full-20260914/`,不含鉴权秘密。
|
||||||
|
|
||||||
|
真实缺陷:Webhook在Node自动地址族选择时因lookup回调格式错误,未到HTTP接收端就报Invalid IP address: undefined。最小修复保持SSRF地址校验不变;API 73套783项及构建通过,已授权提交/推送/测试发布,发布及回调复验未完成。
|
||||||
|
|
||||||
|
共享模拟接入号造成3条上行ambiguous,公开接口正确隐藏;独立接入号后的3条上行已正确匹配。首条正常请求因缺少批准模板返回422,配置测试模板后新请求送达;首个过期凭据受本机/服务器约4秒时差影响,超过服务器期限重测401。保留原结果,不计为服务端缺陷。
|
||||||
|
|
||||||
|
待完成:标准发布、Webhook成功和失败重试/超时、最终PG/Redis/计费对账、测试设施收尾;错误JSON的统一问题响应和未知字段处理须按契约单列评估。未触碰预生产、真实通道、原客户配置、余额或历史短信重投。
|
||||||
@@ -5455,3 +5455,15 @@ TC-CHANNEL-WORD测试部署验收:应用8e4bc5a;新Tab/API200真实空词库
|
|||||||
### TC-HTTP-REMED-IMPL-20260914 测试环境补验
|
### TC-HTTP-REMED-IMPL-20260914 测试环境补验
|
||||||
|
|
||||||
实际发布f0e8434,标准preflight/prepare/deploy/verify通过,迁移、服务、队列、日志和恢复资产已检查。测试公开文档页三尺寸/检索/示例/复制降级/下载/刷新及真实JSON/401关联ID通过。已登录客户端完整交互、真实发送与计费/Gateway供应商链路未验证,未获专项发送授权;不将隔离和公开页结果写成这几项通过。证据见[最终发布记录](release-20260914-test-http-api.md)。
|
实际发布f0e8434,标准preflight/prepare/deploy/verify通过,迁移、服务、队列、日志和恢复资产已检查。测试公开文档页三尺寸/检索/示例/复制降级/下载/刷新及真实JSON/401关联ID通过。已登录客户端完整交互、真实发送与计费/Gateway供应商链路未验证,未获专项发送授权;不将隔离和公开页结果写成这几项通过。证据见[最终发布记录](release-20260914-test-http-api.md)。
|
||||||
|
|
||||||
|
|
||||||
|
## HTTP全量真实模拟链路验收(2026-09-14)
|
||||||
|
|
||||||
|
- TC-HTTP-FULL-AUTH:四接口鉴权缺失/错误、时间、nonce、凭据撤销/过期、IP、权限和QPS;查询只读隔离到本轮新应用。
|
||||||
|
- TC-HTTP-FULL-PARAM:手机号、正文、客户编号、幂等键及query的类型、长度、重复、日期、cursor、limit边界;异常不得创建短信。
|
||||||
|
- TC-HTTP-FULL-SEND:三网、长短信、UTF8转义、同键重放/冲突及5请求并发,PG业务记录、实际CMPP Submit、回执、队列和计费对账;仅虚构号码和既有LGST模拟通道。
|
||||||
|
- TC-HTTP-FULL-MO:真实CMPP DELIVER、ACK、PG落库、唯一/歧义归属、分页筛选、公共字段白名单、同租户跨应用和跨租户不可见。
|
||||||
|
- TC-HTTP-FULL-HOOK:真实公网HTTPS收件端,验证原始体HMAC、eventId去重、2xx、400终止、429/503/超时重试、次数/退避、人工重试;仅本轮事件。Node all-address lookup必须用真实HTTP连接回归,不能只mock transport。
|
||||||
|
- TC-HTTP-FULL-CLOSE:新测试凭据停用、专用应用停止测试、本轮receiver/tunnel/simulator关闭;原配置和历史记录保留,不手工修改余额或清队列。
|
||||||
|
|
||||||
|
执行结果与限制见 [HTTP全量验收](http-api-full-acceptance-20260914.md),用例存在不表示全部通过。
|
||||||
|
|||||||
@@ -4936,3 +4936,8 @@ git diff --check
|
|||||||
认证入口纠正后,标准LF预检发现旧源码CRLF差异;逐字节确认后新建CRLF计划20260914T050659-f0e843436c71-48bc2b23,复用同一精确提交有效证据,重新preflight通过。prepare→deploy→独立verify→report完成,测试实际应用f0e843436c715010d3eaec72a5e0c81816a6e5bd,服务正常、verificationWarnings为空;独立恢复备份约496.57MB保留,停止至恢复约13.5秒。
|
认证入口纠正后,标准LF预检发现旧源码CRLF差异;逐字节确认后新建CRLF计划20260914T050659-f0e843436c71-48bc2b23,复用同一精确提交有效证据,重新preflight通过。prepare→deploy→独立verify→report完成,测试实际应用f0e843436c715010d3eaec72a5e0c81816a6e5bd,服务正常、verificationWarnings为空;独立恢复备份约496.57MB保留,停止至恢复约13.5秒。
|
||||||
|
|
||||||
测试实际公开文档/JSON为200,七query/唯一幂等头/未认证401关联ID通过;Edge三尺寸、检索、示例、复制降级、同字节下载和刷新通过,无pageerror。未将公开页验收冒称已登录客户端完整验收,未发送/补发/重投/入队短信,未作真实计费/供应商链路测试。测试系统盘78%、可用22,848,892,928字节,51目录容量清单和上一有效8e4bc5a组件保留;历史资产清理目标未完成。密码仅标准输入传递,不写入文件;原认证/路径/换行失败和等待不并入停机耗时。详见[最终发布记录](release-20260914-test-http-api.md)。预生产未操作,文档收尾提交不改变测试选定应用版本。
|
测试实际公开文档/JSON为200,七query/唯一幂等头/未认证401关联ID通过;Edge三尺寸、检索、示例、复制降级、同字节下载和刷新通过,无pageerror。未将公开页验收冒称已登录客户端完整验收,未发送/补发/重投/入队短信,未作真实计费/供应商链路测试。测试系统盘78%、可用22,848,892,928字节,51目录容量清单和上一有效8e4bc5a组件保留;历史资产清理目标未完成。密码仅标准输入传递,不写入文件;原认证/路径/换行失败和等待不并入停机耗时。详见[最终发布记录](release-20260914-test-http-api.md)。预生产未操作,文档收尾提交不改变测试选定应用版本。
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-09-14 真实HTTP全量验收发现的Webhook DNS缺陷
|
||||||
|
|
||||||
|
测试环境 f0e8434 上,专用模拟应用已产生真实送达回执,但5个Webhook事件均在2次尝试后失败,lastError为 `Invalid IP address: undefined`,受控HTTPS接收端无请求。Node自动地址族选择以 `all=true` 调用自定义lookup,旧实现仍返回单地址三参数,违反回调契约。保留原SSRF解析、私网拒绝及地址固定,只按all选项返回固定地址数组或原单地址。未改变重试、计费、发送与租户规则。真实Node HTTPS连接旧实现失败/修正实现200,本地真实HTTP连接回归及API全量73套783项、类型构建通过;线上修复与Webhook全场景验收尚待标准发布完成。用户已另行授权本修复提交、推送及测试发布,不涉及预生产。完整结果将登记 [HTTP全量验收](http-api-full-acceptance-20260914.md)。
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
// Real CMPP 2 TCP test peers. No real supplier forwarding exists in this module.
|
||||||
|
import net from 'node:net';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
const z = (n) => Buffer.alloc(n);
|
||||||
|
const fixed = (s, n) => {
|
||||||
|
const b = z(n);
|
||||||
|
b.write(s ?? '', 'ascii');
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
const u32 = (n) => {
|
||||||
|
const b = z(4);
|
||||||
|
b.writeUInt32BE(n >>> 0);
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
const u64 = (n) => {
|
||||||
|
const b = z(8);
|
||||||
|
b.writeBigUInt64BE(BigInt(n));
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
export function packet(type, sequence, body = z(0)) {
|
||||||
|
return Buffer.concat([u32(body.length + 12), u32(type), u32(sequence), body]);
|
||||||
|
}
|
||||||
|
export function parseSubmit(body) {
|
||||||
|
const count = body[116];
|
||||||
|
const end = 117 + 21 * count;
|
||||||
|
assert(body.length >= end + 9);
|
||||||
|
return {
|
||||||
|
total: body[8],
|
||||||
|
number: body[9],
|
||||||
|
receipt: body[10],
|
||||||
|
udhi: body[45],
|
||||||
|
format: body[46],
|
||||||
|
source: body.subarray(95, 116).toString('ascii').replace(/\0/g, ''),
|
||||||
|
phones: Array.from({ length: count }, (_, i) =>
|
||||||
|
body
|
||||||
|
.subarray(117 + i * 21, 138 + i * 21)
|
||||||
|
.toString('ascii')
|
||||||
|
.replace(/\0/g, ''),
|
||||||
|
),
|
||||||
|
content: body.subarray(end + 1, end + 1 + body[end]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export function decodeUcs2(buffer) {
|
||||||
|
return Buffer.from(buffer).swap16().toString('utf16le');
|
||||||
|
}
|
||||||
|
function framed(socket, onPacket, onError) {
|
||||||
|
let accumulated = z(0);
|
||||||
|
socket.on('error', onError);
|
||||||
|
socket.on('data', (chunk) => {
|
||||||
|
try {
|
||||||
|
accumulated = Buffer.concat([accumulated, chunk]);
|
||||||
|
while (accumulated.length >= 12) {
|
||||||
|
const length = accumulated.readUInt32BE(0);
|
||||||
|
assert(length >= 12 && length <= 65536, 'Invalid CMPP frame length');
|
||||||
|
if (accumulated.length < length) return;
|
||||||
|
const value = accumulated.subarray(0, length);
|
||||||
|
accumulated = accumulated.subarray(length);
|
||||||
|
onPacket(value.readUInt32BE(4), value.readUInt32BE(8), value.subarray(12));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
onError(error);
|
||||||
|
socket.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export async function startSupplier({ host, port, accounts, phoneAllowed, events, receiptStatus = () => 'DELIVRD' }) {
|
||||||
|
assert.equal(host, '127.0.0.1');
|
||||||
|
assert.equal(port, 17900);
|
||||||
|
const sockets = new Set();
|
||||||
|
const connected = new Set();
|
||||||
|
const peers = new Map();
|
||||||
|
let id = BigInt(Date.now()) * 1000n;
|
||||||
|
let sequence = 80000;
|
||||||
|
const server = net.createServer((socket) => {
|
||||||
|
if (socket.remoteAddress?.replace('::ffff:', '') !== '127.0.0.1') {
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sockets.add(socket);
|
||||||
|
let account;
|
||||||
|
socket.on('close', () => {
|
||||||
|
sockets.delete(socket);
|
||||||
|
if (account) connected.delete(account);
|
||||||
|
});
|
||||||
|
framed(
|
||||||
|
socket,
|
||||||
|
(type, seq, body) => {
|
||||||
|
if (type === 1) {
|
||||||
|
account = body.subarray(0, 6).toString('ascii').replace(/\0/g, '');
|
||||||
|
assert(accounts.has(account), 'Unexpected simulator account');
|
||||||
|
assert.equal(body[22], 0x20);
|
||||||
|
socket.write(packet(0x80000001, seq, Buffer.concat([z(17), Buffer.from([0x20])])));
|
||||||
|
connected.add(account);
|
||||||
|
peers.set(account, socket);
|
||||||
|
events.push({ type: 'connect', account, time: Date.now() });
|
||||||
|
} else if (type === 8) socket.write(packet(0x80000008, seq, z(1)));
|
||||||
|
else if (type === 2) {
|
||||||
|
socket.write(packet(0x80000002, seq));
|
||||||
|
socket.end();
|
||||||
|
} else if (type === 4) {
|
||||||
|
assert(account);
|
||||||
|
const submit = parseSubmit(body);
|
||||||
|
assert(submit.phones.every(phoneAllowed), 'Unexpected recipient: simulator stop condition');
|
||||||
|
assert(events.filter((x) => x.type === 'submit').length < 160, 'Wire submit cap reached');
|
||||||
|
const messageId = ++id;
|
||||||
|
events.push({
|
||||||
|
type: 'submit',
|
||||||
|
account,
|
||||||
|
time: Date.now(),
|
||||||
|
id: String(messageId),
|
||||||
|
...submit,
|
||||||
|
content: submit.content.toString('base64'),
|
||||||
|
});
|
||||||
|
socket.write(packet(0x80000004, seq, Buffer.concat([u64(messageId), z(1)])));
|
||||||
|
if (submit.receipt) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (socket.destroyed) return;
|
||||||
|
const now = new Date();
|
||||||
|
const stamp = [
|
||||||
|
now.getFullYear() % 100,
|
||||||
|
now.getMonth() + 1,
|
||||||
|
now.getDate(),
|
||||||
|
now.getHours(),
|
||||||
|
now.getMinutes(),
|
||||||
|
]
|
||||||
|
.map((n) => String(n).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
const report = Buffer.concat([
|
||||||
|
u64(messageId),
|
||||||
|
fixed(receiptStatus(submit), 7),
|
||||||
|
fixed(stamp, 10),
|
||||||
|
fixed(stamp, 10),
|
||||||
|
fixed(submit.phones[0], 21),
|
||||||
|
u32(sequence),
|
||||||
|
]);
|
||||||
|
const deliver = Buffer.concat([
|
||||||
|
u64(messageId),
|
||||||
|
fixed(submit.source, 21),
|
||||||
|
z(10),
|
||||||
|
z(3),
|
||||||
|
fixed(submit.phones[0], 21),
|
||||||
|
Buffer.from([1, report.length]),
|
||||||
|
report,
|
||||||
|
z(8),
|
||||||
|
]);
|
||||||
|
socket.write(packet(5, ++sequence, deliver));
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
} else if (type === 0x80000005)
|
||||||
|
events.push({ type: 'supplierReceiptAck', account, result: body[8], time: Date.now() });
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
events.push({ type: 'error', message: error.message });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
server.once('error', reject);
|
||||||
|
server.listen(port, host, resolve);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
connected,
|
||||||
|
sendUplink(account, phone, dest, text) {
|
||||||
|
const socket = peers.get(account);
|
||||||
|
assert(socket && !socket.destroyed);
|
||||||
|
assert(phoneAllowed(phone));
|
||||||
|
const content = Buffer.from(text, 'utf16le').swap16();
|
||||||
|
assert(content.length <= 140);
|
||||||
|
const messageId = ++id;
|
||||||
|
const body = Buffer.concat([
|
||||||
|
u64(messageId),
|
||||||
|
fixed(dest, 21),
|
||||||
|
z(10),
|
||||||
|
z(2),
|
||||||
|
Buffer.from([8]),
|
||||||
|
fixed(phone, 21),
|
||||||
|
Buffer.from([0, content.length]),
|
||||||
|
content,
|
||||||
|
z(8),
|
||||||
|
]);
|
||||||
|
socket.write(packet(5, ++sequence, body));
|
||||||
|
events.push({ type: 'supplierUplink', account, phone, dest, text, id: String(messageId), time: Date.now() });
|
||||||
|
},
|
||||||
|
close: async () => {
|
||||||
|
for (const socket of sockets) socket.destroy();
|
||||||
|
await new Promise((resolve) => server.close(resolve));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user