event-source-hook
Version:
Easily intercept, modify, and simulate EventSource server-sent events.
234 lines (233 loc) • 10.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
var NativeEventSource = EventSource;
/**
* Create a mutable message event from an immutable message event with the same property values.
* @param messageEvent - Immutable native `MessageEvent`.
* @returns `messageEvent` but mutable.
*/
function ToMutableMessageEvent(messageEvent) {
var self = {};
self.bubbles = messageEvent.bubbles || false;
self.cancelable = messageEvent.cancelable || false;
self.cancelBubble = messageEvent.cancelBubble || false;
self.composed = messageEvent.composed || false;
self.currentTarget = messageEvent.currentTarget || null;
self.data = messageEvent.data || null;
self.defaultPrevented = messageEvent.defaultPrevented || false;
self.eventPhase = messageEvent.eventPhase || 0;
self.isTrusted = messageEvent.isTrusted || false;
self.lastEventId = messageEvent.lastEventId || "";
self.origin = messageEvent.origin || "";
self.ports = messageEvent.ports || [];
self.returnValue = messageEvent.returnValue || true;
self.simulated = messageEvent.simulated || false;
self.source = messageEvent.source || null;
self.srcElement = messageEvent.srcElement || null;
self.target = messageEvent.target || null;
self.timeStamp = messageEvent.timeStamp || performance.now();
self.type = messageEvent.type || "message";
Object.setPrototypeOf(self, Object.getPrototypeOf(messageEvent));
return self;
}
/**
* Create a spoofed `EventSource` object for hook. Swapped with the native `EventSource`.
* @constructor
*/
function HookedEventSource(url, eventSourceInitDict) {
var _a, _b;
// Call the URL hook function to eventually get a new URL.
url = ((_a = ESHook.urlHook) === null || _a === void 0 ? void 0 : _a.call(ESHook, String(url))) || url;
var es = new NativeEventSource(url, eventSourceInitDict);
es._mapListenerProxy = new WeakMap();
es._nativeAddEventListener = es.addEventListener;
es._nativeRemoveEventListener = es.removeEventListener;
/* ----------------------- Spoof `addEventListener()` ----------------------- */
es._createEventProxy = function (listener) {
return function (event) {
var mutableEvent = ToMutableMessageEvent(event);
// Set `isTrusted` to `true` like an genuine event.
if (mutableEvent.simulated)
mutableEvent.isTrusted = true;
// If no hook function, directly call listener.
if (!ESHook.eventHook)
return listener(mutableEvent);
var callback = function (mutableEvent) {
if (mutableEvent === null)
return; // If the event is null, then block the event.
listener(mutableEvent);
};
if (ESHook.isEventHookAsync) {
ESHook.eventHook(mutableEvent.type, mutableEvent, es, callback);
}
else {
callback(ESHook.eventHook(mutableEvent.type, mutableEvent, es));
}
};
};
// @ts-ignore
es.addEventListener = function (type, listener, options) {
var _a;
// Ignore these types that are not event message.
if (type === "open" || type === "error") {
return this._nativeAddEventListener(type, listener, options);
}
// Throw error if wrong listener type like official impl.
if (!["function", "object"].includes(typeof listener)) {
throw new TypeError("Failed to execute 'addEventListener' on 'EventTarget': parameter 2 is not of type 'Object'.");
}
// Get event handler function if listener is an object.
if (typeof listener === "object") {
listener = listener.handleEvent;
if (typeof listener !== "function")
return;
}
// Proxy function to be called instead of listener.
var proxy = this._createEventProxy(listener);
this._nativeAddEventListener(type, proxy, options);
// Store (listener -> proxy) to be able to remove the listener later.
var proxies = (_a = this._mapListenerProxy.get(listener)) !== null && _a !== void 0 ? _a : {};
proxies[type] = proxy;
this._mapListenerProxy.set(listener, proxies);
};
/* ---------------------- Spoof `removeEventListener()` --------------------- */
// @ts-ignore
es.removeEventListener = function (type, listener, options) {
// Ignore these types that are not event message.
if (type === "open" || type === "error") {
return this._nativeRemoveEventListener(type, listener, options);
}
var proxies = this._mapListenerProxy.get(listener);
var proxy = proxies === null || proxies === void 0 ? void 0 : proxies[type];
if (proxy) {
this._nativeRemoveEventListener(type, proxy, options);
delete proxies[type];
}
};
/* ---------------------------- Spoof `onmessage` --------------------------- */
Object.defineProperty(es, "onmessage", {
get: function () {
return this._onmessage;
},
set: function (listener) {
if (typeof listener === "function") {
this.addEventListener("message", listener);
}
else {
listener = null;
}
this.removeEventListener("message", this._onmessage);
this._onmessage = listener;
},
});
es.onmessage = null;
/* -------------------------------------------------------------------------- */
// Call the create hook function before returning the instance.
(_b = ESHook.createHook) === null || _b === void 0 ? void 0 : _b.call(ESHook, es);
return es;
}
/** Library `event-source-hook` static class that provides `EventSource` hooking utilities. */
var ESHook = /** @class */ (function () {
function ESHook() {
}
Object.defineProperty(ESHook, "urlHook", {
/* ------------------------------- Properties ------------------------------- */
/** Hook function invoked just before a connection is established. */
get: function () {
return this._urlHook;
},
/** Hook function invoked just before a connection is established. */
set: function (func) {
this._urlHook = typeof func === "function" ? func : null;
},
enumerable: false,
configurable: true
});
Object.defineProperty(ESHook, "createHook", {
/** Hook function invoked just before a connection is established. */
get: function () {
return this._createHook;
},
/** Hook function invoked just before a connection is established. */
set: function (func) {
this._createHook = typeof func === "function" ? func : null;
},
enumerable: false,
configurable: true
});
Object.defineProperty(ESHook, "eventHook", {
/** Hook function invoked just before an event is received on any connection. */
get: function () {
return this._eventHook;
},
/** Hook function invoked just before an event is received on any connection. */
set: function (func) {
this._eventHook = typeof func === "function" ? func : null;
},
enumerable: false,
configurable: true
});
Object.defineProperty(ESHook, "isEventHookAsync", {
/** Is the provided event hook function async? */
get: function () {
return typeof this._eventHook === "function" && this._eventHook.length >= 4;
},
enumerable: false,
configurable: true
});
Object.defineProperty(ESHook, "enabled", {
/** Is the native `EventSource` spoofed by the library? */
get: function () {
return EventSource.name === "HookedEventSource";
},
enumerable: false,
configurable: true
});
/* --------------------------------- Methods -------------------------------- */
/**
* Swap the native `EventSource` constructor with a spoofed one.
* Any opened connections will be spoofed by the library while enabled.
*/
ESHook.enable = function () {
// @ts-ignore
EventSource = HookedEventSource;
};
/**
* Swap the native `EventSource` constructor back in.
* Any opened connections while disabled will not be spoofed by the library even after re-enabling.
* @note Hook event function will still be called while disabled.
*/
ESHook.disable = function () {
// @ts-ignore
EventSource = NativeEventSource;
};
/**
* Reset all hooks function to none.
*/
ESHook.resetHooks = function () {
this._urlHook = null;
this._createHook = null;
this._eventHook = null;
};
/**
* Simulate a received event. It will be handled as if it was an genuine event received from the server.
* @note `simulated` and `isTrusted` properties are set to `true` on the `MessageEvent` object.
* @param eventSource - Connection where the event should be received.
* @param type - Event type. Use `message` if you want a non-typed event (default type).
* @param options - Options to be passed to the event (such as data).
*/
ESHook.simulate = function (eventSource, type, options) {
var _a;
if (options === void 0) { options = {}; }
options.origin = (_a = options.origin) !== null && _a !== void 0 ? _a : new URL(eventSource.url).origin;
options.data = JSON.stringify(options.data);
var event = new MessageEvent(type, options);
event.simulated = true;
eventSource.dispatchEvent(event);
};
ESHook._urlHook = null;
ESHook._createHook = null;
ESHook._eventHook = null;
return ESHook;
}());
exports.default = ESHook;