Files
jerapah-flow/plugins/send-sms/script.js
T
nsrb 707e2c0f1e 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.
2026-09-08 22:58:52 +07:00

219 lines
6.3 KiB
JavaScript

import net from "node:net";
function passContext(ctx) {
if (ctx?.context != null && typeof ctx.context === "object" && !Array.isArray(ctx.context)) {
return { ...ctx.context };
}
return {};
}
function requiredString(value, label) {
if (typeof value !== "string" || value.length === 0) {
throw new Error(`${label} is required`);
}
return value;
}
function pad2(n) {
return String(n).padStart(2, "0");
}
function formatSmsTime(date = new Date()) {
const offsetMinutes = -date.getTimezoneOffset();
const sign = offsetMinutes >= 0 ? "+" : "-";
const absHours = Math.abs(offsetMinutes) / 60;
const tz = Number.isInteger(absHours) ? String(absHours) : String(absHours);
return [
pad2(date.getFullYear() % 100),
pad2(date.getMonth() + 1),
pad2(date.getDate()),
pad2(date.getHours()),
pad2(date.getMinutes()),
pad2(date.getSeconds()),
`${sign}${tz}`,
].join(";");
}
function encodeUtf16BeHex(text) {
let out = "";
for (let i = 0; i < text.length; i++) {
out += text.charCodeAt(i).toString(16).toUpperCase().padStart(4, "0");
}
return out;
}
function isGsm7(text) {
const basic =
"@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?" +
"¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà";
const ext = new Set(["\f", "^", "{", "}", "\\", "[", "~", "]", "|", "€"]);
for (const ch of text) {
if (!basic.includes(ch) && !ext.has(ch)) return false;
}
return true;
}
function refererFromUrl(url) {
return `${new URL(url).origin}/index.html`;
}
function parseResult(text) {
const match = String(text).match(/\{[^{}]*"result"\s*:\s*"[^"]*"[^{}]*\}/);
if (!match) return { raw: String(text) };
try {
return JSON.parse(match[0]);
} catch {
return { raw: String(text) };
}
}
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})`);
}
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) {
return zteHttpPost(url, fields);
}
async function sendZteSms(ctx) {
const url = requiredString(ctx.config?.url, "config.url");
const password = requiredString(ctx.config?.password, "config.password");
const to = requiredString(ctx.data?.to ?? ctx.data?.number, "data.to");
const message = requiredString(ctx.data?.message, "data.message");
log.info({ url }, "send-sms: logging in");
const login = await ztePost(url, {
goformId: "LOGIN",
password: btoa(password),
});
const loginResult = login.result;
if (loginResult !== "0" && loginResult !== 0 && loginResult !== "3" && loginResult !== 3 && loginResult !== "success") {
throw new Error(`ZTE login failed: ${JSON.stringify(login.result ?? login)}`);
}
const encodeType = isGsm7(message) ? "GSM7_default" : "UNICODE";
const smsTime = formatSmsTime();
log.info({ url, to, encodeType, messageLength: message.length }, "send-sms: sending");
const sent = await ztePost(url, {
goformId: "SEND_SMS",
notCallback: "true",
Number: to,
sms_time: smsTime,
MessageBody: encodeUtf16BeHex(message),
ID: "-1",
encode_type: encodeType,
});
if (sent.result !== "success" && sent.result !== "0" && sent.result !== 0) {
throw new Error(`ZTE SEND_SMS failed: ${JSON.stringify(sent.result ?? sent)}`);
}
log.info({ to, result: sent.result }, "send-sms: sent");
return {
output: { sent: true, to, result: sent.result },
context: passContext(ctx),
};
}
sendZteSms.meta = {
description: "Log in to a ZTE modem web UI and send an SMS",
previewConfigKey: "url",
tags: ["channel"],
config: {
url: {
type: "string",
required: true,
default: "http://192.168.5.1/reqproc/proc_post",
description: "Modem POST URL (LOGIN and SEND_SMS)",
},
password: {
type: "string",
required: true,
description: "Web UI password (sent as base64)",
},
},
input: {
to: { type: "string", required: true, description: "Recipient phone number" },
message: { type: "string", required: true, description: "SMS text" },
},
output: {
sent: { type: "boolean" },
to: { type: "string" },
result: { type: "string" },
},
example: {
data: {
to: "082134336193",
message: "Test nomor kirim aja dari XL prioritas",
},
config: {
url: "http://192.168.5.1/reqproc/proc_post",
password: "error403",
},
},
};
export default sendZteSms;