Files
escProxy/index.js
2026-07-09 10:57:37 +02:00

170 lines
5.0 KiB
JavaScript

import net from "node:net";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import dotenv from "dotenv";
import { parse, renderTokens, serialize } from "./parser.js";
dotenv.config();
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const num = (v, d) => (v === undefined || v === "" ? d : Number(v));
const str = (v, d) => (v === undefined || v === "" ? d : v);
const bool = (v, d) =>
v === undefined || v === "" ? d : v.toLowerCase() === "true" || v === "1";
// Parse CUT_PREFIX (hex, space-separated) into a byte array.
function parseCutPrefix(v, d) {
if (v === undefined || v === "") return d;
return v
.trim()
.split(/\s+/)
.map((h) => parseInt(h, 16));
}
const config = {
listenHost: str(process.env.LISTEN_HOST, "0.0.0.0"),
listenPort: num(process.env.LISTEN_PORT, 9100),
targetHost: str(process.env.TARGET_HOST, "10.10.10.24"),
targetPort: num(process.env.TARGET_PORT, 9100),
logDir: str(process.env.LOG_DIR, "./logs"),
forward: bool(process.env.FORWARD, true),
cutPrefix: parseCutPrefix(process.env.CUT_PREFIX, [0x1d, 0x56]),
keepAfterCut: bool(process.env.KEEP_AFTER_CUT, true),
};
const logDir = path.resolve(__dirname, config.logDir);
fs.mkdirSync(logDir, { recursive: true });
let seq = 0;
function timestamp() {
// ISO timestamp without colons (filesystem friendly), with ms.
return new Date().toISOString().replace(/[:.]/g, "-");
}
function logJob(buffer) {
if (!buffer || buffer.length === 0) return;
const id = `${timestamp()}-${String(++seq).padStart(4, "0")}`;
const parsedPath = path.join(logDir, `job-${id}.log`);
// Only the parsed, lossless token stream is logged.
let parsedLines = "";
try {
const events = parse(buffer);
parsedLines = renderTokens(events);
// Sanity: the parser must be lossless (rebuild == original).
const rebuilt = serialize(events);
if (!rebuilt.equals(buffer)) {
throw new Error("parser round-trip mismatch");
}
} catch (err) {
parsedLines = `[parse error] ${err.message}\n${buffer.toString("latin1")}`;
}
fs.writeFileSync(parsedPath, parsedLines);
console.log(`[log] job ${id} (${buffer.length} bytes) -> ${parsedPath}`);
}
// Find the end offset (exclusive) of the first complete cut command in `buf`.
// Returns -1 if no complete cut sequence is present yet.
// A cut is: cutPrefix (0x1D 0x56) followed by 1 mode byte.
function findCutEnd(buf, cutPrefix) {
const n = cutPrefix.length;
let i = 0;
while (i <= buf.length - (n + 1)) {
let match = true;
for (let k = 0; k < n; k++) {
if (buf[i + k] !== cutPrefix[k]) {
match = false;
break;
}
}
if (match) {
// cut + mode byte => end is i + n + 1
return i + n + 1;
}
i++;
}
return -1;
}
// Streams received bytes to the target printer (unless forwarding is disabled).
function connectTarget() {
if (!config.forward) return null;
const target = net.createConnection(
{ host: config.targetHost, port: config.targetPort },
() => console.log(`[target] connected to ${config.targetHost}:${config.targetPort}`)
);
target.on("error", (err) => {
console.error(`[target] error: ${err.message}`);
target.destroy();
});
target.on("close", () => console.log("[target] connection closed"));
return target;
}
const server = net.createServer((clientSocket) => {
const clientAddr = `${clientSocket.remoteAddress}:${clientSocket.remotePort}`;
console.log(`[client] connected ${clientAddr}`);
let buffer = Buffer.alloc(0);
const target = connectTarget();
clientSocket.on("data", (chunk) => {
// Forward raw bytes straight to the printer (if forwarding is enabled).
if (target && target.writable) target.write(chunk);
buffer = Buffer.concat([buffer, chunk]);
// Finalize any complete jobs delimited by a cut command.
let cutEnd = findCutEnd(buffer, config.cutPrefix);
while (cutEnd !== -1) {
const job = buffer.subarray(0, cutEnd);
logJob(job);
if (config.keepAfterCut) {
buffer = buffer.subarray(cutEnd);
cutEnd = findCutEnd(buffer, config.cutPrefix);
} else {
buffer = Buffer.alloc(0);
break;
}
}
});
const finalize = () => {
// Connection drop: any remaining bytes form the last job.
if (buffer.length > 0) {
logJob(buffer);
buffer = Buffer.alloc(0);
}
if (target) target.end();
};
clientSocket.on("end", () => {
console.log(`[client] end ${clientAddr}`);
finalize();
});
clientSocket.on("close", () => {
console.log(`[client] closed ${clientAddr}`);
finalize();
});
clientSocket.on("error", (err) => {
console.error(`[client] error ${clientAddr}: ${err.message}`);
finalize();
});
});
server.listen(config.listenPort, config.listenHost, () => {
console.log(
`[server] listening on ${config.listenHost}:${config.listenPort}, ` +
(config.forward
? `forwarding to ${config.targetHost}:${config.targetPort}, `
: `forwarding DISABLED (receive + log only), `) +
`logging to ${logDir}`
);
});