feat(plugins): enhance SMS sending functionality with raw HTTP handling

- Added support for using the `net` module in the `send-sms` plugin to establish raw HTTP connections.
- Refactored the `ztePost` function to `zteHttpPost`, allowing for custom HTTP request formatting and improved error handling.
- Introduced a new utility function, `httpBodyFromRaw`, to process raw HTTP responses effectively.
- Updated the `script-sandbox.js` to allow the use of the `net` and `node:net` modules in the sandbox environment.
This commit is contained in:
2026-09-08 22:58:52 +07:00
parent 6c82ff20eb
commit 707e2c0f1e
2 changed files with 69 additions and 28 deletions
+2
View File
@@ -22,7 +22,9 @@ const ALLOWED_MODULES = new Set([
"basic-ftp",
"jsonata",
"mustache",
"net",
"node-html-parser",
"node:net",
"node:stream",
"nodemailer",
"rss-parser",
+67 -28
View File
@@ -1,3 +1,5 @@
import net from "node:net";
function passContext(ctx) {
if (ctx?.context != null && typeof ctx.context === "object" && !Array.isArray(ctx.context)) {
return { ...ctx.context };
@@ -65,38 +67,75 @@ function parseResult(text) {
}
}
function leftoverFromError(err) {
let current = err;
for (let i = 0; i < 6 && current; i++) {
if (typeof current.data === "string") return current.data;
current = current.cause;
function httpBodyFromRaw(raw) {
const normalized = String(raw).replace(/\r\n/g, "\n");
const split = normalized.indexOf("\n\n");
return split === -1 ? normalized : normalized.slice(split + 2);
}
/**
* ZTE Demo-Webs uses LF-only status lines (`HTTP/1.1 200 OK\n`).
* Node 24 fetch/http throw HPE_CR_EXPECTED before any body is read.
*/
function zteHttpPost(url, fields) {
const target = new URL(url);
if (target.protocol !== "http:") {
throw new Error(`send-sms: only http: is supported (got ${target.protocol})`);
}
return null;
const body = new URLSearchParams(fields).toString();
const request = [
`POST ${target.pathname}${target.search} HTTP/1.1`,
`Host: ${target.host}`,
"accept: application/json, text/javascript, */*; q=0.01",
"accept-language: en-US,en;q=0.9",
"content-type: application/x-www-form-urlencoded; charset=UTF-8",
"x-requested-with: XMLHttpRequest",
`Referer: ${refererFromUrl(url)}`,
`Content-Length: ${Buffer.byteLength(body)}`,
"Connection: close",
"",
body,
].join("\r\n");
return new Promise((resolve, reject) => {
const socket = net.connect(
{ host: target.hostname, port: Number(target.port) || 80 },
() => {
socket.write(request);
},
);
const chunks = [];
let settled = false;
const finish = (raw) => {
if (settled) return;
settled = true;
socket.removeAllListeners();
socket.destroy();
resolve(parseResult(httpBodyFromRaw(raw)));
};
socket.on("data", (chunk) => {
chunks.push(chunk);
const raw = Buffer.concat(chunks).toString("utf8");
const parsed = parseResult(httpBodyFromRaw(raw));
if (parsed.result !== undefined) finish(raw);
});
socket.on("end", () => finish(Buffer.concat(chunks).toString("utf8")));
socket.on("error", (err) => {
if (settled) return;
settled = true;
reject(err);
});
socket.setTimeout(15000, () => {
if (settled) return;
settled = true;
socket.destroy();
reject(new Error("ZTE HTTP timeout"));
});
});
}
async function ztePost(url, fields) {
const body = new URLSearchParams(fields).toString();
try {
const response = await fetch(url, {
method: "POST",
headers: {
accept: "application/json, text/javascript, */*; q=0.01",
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"x-requested-with": "XMLHttpRequest",
Referer: refererFromUrl(url),
},
body,
});
return parseResult(await response.text());
} catch (err) {
const leftover = leftoverFromError(err);
if (leftover != null) {
const parsed = parseResult(leftover);
log.info({ result: parsed.result ?? parsed }, "send-sms: recovered malformed response");
return parsed;
}
throw err;
}
return zteHttpPost(url, fields);
}
async function sendZteSms(ctx) {