@gr2m/net-interceptor
Version:
Intercept and mock outgoing network TCP/TLS connections
158 lines (130 loc) • 4.07 kB
JavaScript
// @ts-check
import stream from "node:stream";
import NODE_INTERNALS from "./node-internals.js";
const NO_ERROR_CODE = 0;
const UV_EOF = NODE_INTERNALS.UV_EOF;
export const kRemote = Symbol("Remote");
let uniqueId = 0;
let STREAM_STATE = NODE_INTERNALS.STREAM_STATE;
let STREAM_BYTES_READ = NODE_INTERNALS.STREAM_BYTES_READ;
export default function createRequestResponseHandlePair() {
const requestHandle = new StreamHandle();
const responseHandle = new StreamHandle();
requestHandle[kRemote] = responseHandle;
responseHandle[kRemote] = requestHandle;
return { requestHandle, responseHandle };
}
/**
* Sockets write to StreamHandle via write*String functions. The
* WritableStream.prototype.write function is just used internally by
* StreamHandle to queue data before pushing it to the other end via
* ReadableStream.prototype.push. The receiver will then forward it to its
* owner Socket via the onread property.
*
* StreamHandle is created for both the request side and the response side.
*/
class StreamHandle extends stream.Duplex {
remote = null;
constructor() {
super();
this.id = ++uniqueId;
// The "end" event follows ReadableStream.prototype.push(null).
this.on("data", readData.bind(this));
this.on("end", readEof.bind(this));
// The "finish" event follows WritableStream.prototype.end.
//
// There's WritableStream.prototype._final for processing before "finish" is
// emitted, but that's only available in Node v8 and later.
this.on(
"finish",
this._write.bind(this, null, null, () => {})
);
this.pause();
}
readStart() {
this.resume();
}
// readstop is not called by Node 17
/* c8 ignore start */
readStop() {
this.pause();
}
/* c8 ignore stop */
// noops
_read() {}
ref() {}
unref() {}
getAsyncId() {
return this.id;
}
_write(data, encoding, done) {
const remote = this[kRemote];
process.nextTick(function () {
remote.push(data, encoding);
done();
});
}
// Node requires writev to be set on the handler because, while
// WritableStream expects _writev, internal/stream_base_commons.js calls
// req.handle.writev directly. It's given a flat array of data+type pairs.
writev(_req, data) {
for (let i = 0; i < data.length; ++i)
this._write(data[i], data[++i], () => {});
return NO_ERROR_CODE;
}
writeLatin1String(_req, data) {
this.write(data, "latin1");
return NO_ERROR_CODE;
}
writeBuffer(request, data) {
this.write(data);
return NO_ERROR_CODE;
}
writeUtf8String(request, data) {
this.write(data, "utf8");
return NO_ERROR_CODE;
}
writeAsciiString(request, data) {
this.write(data, "ascii");
return NO_ERROR_CODE;
}
writeUcs2String(request, data) {
this.write(data, "ucs2");
return NO_ERROR_CODE;
}
// While it seems to have existed since Node v0.10, Node v11.2 requires
// "shutdown". AFAICT, "shutdown" is for shutting the writable side down and
// hence the use of WritableStream.prototype.end and waiting for the "finish"
// event.
shutdown(request) {
this.once(
"finish",
request.oncomplete.bind(request, NO_ERROR_CODE, request.handle)
);
this.end();
// Must return an error code, where `1` indicating a "synchronous finish"
// (as per Node's net.js) and `0` presumably success.
return 0;
}
// Unsure of the relationship between StreamHandle.prototype.shutdown and
// StreamHandle.prototype.close.
close(done) {
// @ts-expect-error - `._writeableState` is an internal API and not typed
if (!this._writableState.finished) {
this.end(done);
return;
}
/* istanbul ignore next */
if (done) done();
}
}
function readData(data) {
// A system written not in 1960 that passes arguments to functions through
// _global_ mutable data structures…
STREAM_STATE[STREAM_BYTES_READ] = data.length;
this.onread(data);
}
function readEof() {
STREAM_STATE[STREAM_BYTES_READ] = UV_EOF;
this.onread();
}