blob: 14d2aae902770ea472389ec602b9fe0af65247d6 [file] [log] [blame]
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +01001import {waitFor} from 'poll-until-promise';
2
Adrià Vilanova Martínez4e0cb182022-06-26 00:21:50 +02003import {correctArrayKeys} from '../common/protojs';
Adrià Vilanova Martínez47c4c812024-12-05 15:34:40 +01004import ResponseModifier from '../xhrInterceptor/ResponseModifier';
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +01005import * as utils from '../xhrInterceptor/utils.js';
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +02006
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +01007const kSpecialEvents = ['load', 'loadend'];
8const kErrorEvents = ['error', 'timeout', 'abort'];
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +02009
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010010const kStandardMethods = [
11 'open', 'abort', 'setRequestHeader', 'send', 'getResponseHeader',
12 'getAllResponseHeaders', 'dispatchEvent', 'overrideMimeType'
13];
14const kStandardScalars = [
Adrià Vilanova Martínez48dbe7c2024-02-26 04:40:34 +010015 'onabort',
16 'onerror',
17 'onload',
18 'onloadstart',
19 'onloadend',
20 'onprogress',
21 'onreadystatechange',
22 'readyState',
23 'responseType',
24 'responseURL',
25 'responseXML',
26 'status',
27 'statusText',
28 'upload',
29 'withCredentials',
30 'DONE',
31 'UNSENT',
32 'HEADERS_RECEIVED',
33 'LOADING',
34 'OPENED',
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010035];
36
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010037const kCheckInterceptionOptions = {
38 interval: 50,
39 timeout: 100 * 1000,
40};
avm999631f50f6f2021-08-12 23:04:41 +020041
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010042/**
43 * Implements the flatten options concept of the DOM spec.
44 *
45 * @see {@link https://dom.spec.whatwg.org/#concept-flatten-options}
46 */
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010047function flattenOptions(options) {
48 if (typeof options === 'boolean') return options;
49 if (options) return options['capture'];
50 return undefined;
51}
52
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010053/**
54 * Class which, when instantiated, overrides window.XMLHttpRequest to proxy the
55 * requests through our internal interceptors to read/modify requests/responses.
56 *
57 * Slightly based in https://stackoverflow.com/a/24561614.
58 */
Adrià Vilanova Martínez4757ae82022-12-28 16:41:37 +010059export default class XHRProxy {
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010060 constructor() {
61 this.originalXMLHttpRequest = window.XMLHttpRequest;
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +020062
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010063 this.messageID = 0;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010064 this.responseModifier = new ResponseModifier();
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010065
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010066 this.#overrideXHRObject();
67 }
68
69 #overrideXHRObject() {
70 this.#overrideConstructor();
71 this.#addProxyEventsMethods();
72 this.#overrideMethods();
73 this.#addMethodInterceptors();
74 this.#overrideScalars();
75 this.#setOriginalResponseScalar();
76 }
77
78 #overrideConstructor() {
79 const XHRProxyInstance = this;
80
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010081 window.XMLHttpRequest = function() {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010082 this.xhr = new XHRProxyInstance.originalXMLHttpRequest();
83 this.$TWPTID = XHRProxyInstance.messageID++;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010084 this.$responseModified = false;
85 this.$responseIntercepted = false;
86 this.specialHandlers = {
87 load: new Set(),
88 loadend: new Set(),
89 };
90
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010091 this.$proxyEvents();
92 };
93 }
94
95 #addProxyEventsMethods() {
96 window.XMLHttpRequest.prototype.$proxyEvents = function() {
97 this.$proxySpecialEvents();
98 this.$proxyErrorEvents();
99 };
100 this.#addProxySpecialEvents();
101 this.#addProxyErrorEvents();
102 }
103
104 #overrideMethods() {
105 this.#overrideStandardMethods();
106 this.#overrideEventsMethods();
107 }
108
109 #addMethodInterceptors() {
110 this.#addOpenInterceptor();
111 this.#addSetRequestHeaderInterceptor();
112 this.#addSendInterceptor();
113 }
114
115 #overrideScalars() {
116 this.#overrideStandardScalars();
117 this.#overrideResponseScalars();
118 }
119
120 #overrideStandardMethods() {
121 kStandardMethods.forEach(method => {
122 this.#overrideStandardMethod(method);
123 });
124 }
125
126 #overrideEventsMethods() {
127 this.#overrideAddEventListener();
128 this.#overrideRemoveEventListener();
129 }
130
131 #overrideStandardScalars() {
132 kStandardScalars.forEach(scalar => {
133 this.#overrideStandardScalar(scalar);
134 });
135 }
136
137 #overrideResponseScalars() {
138 this.#overrideResponse();
139 this.#overrideResponseText();
140 }
141
142 #addProxySpecialEvents() {
143 const XHRProxyInstance = this;
144 window.XMLHttpRequest.prototype.$proxySpecialEvents = function() {
145 const proxyInstance = this;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100146 kSpecialEvents.forEach(eventName => {
Adrià Vilanova Martínez47c4c812024-12-05 15:34:40 +0100147 this.xhr.addEventListener(eventName, async function() {
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100148 if (eventName === 'load') {
Adrià Vilanova Martínez47c4c812024-12-05 15:34:40 +0100149 const initialResponse = utils.getResponseJSON({
150 responseType: proxyInstance.xhr.responseType,
151 response: proxyInstance.xhr.response,
152 $TWPTRequestURL: proxyInstance.$TWPTRequestURL,
153 $isArrayProto: proxyInstance.$isArrayProto,
154 });
155
156 const result = await XHRProxyInstance.responseModifier.intercept({
157 url: proxyInstance.$TWPTRequestURL,
158 originalResponse: initialResponse,
159 });
160
161 if (result.wasModified) {
162 proxyInstance.$newResponse = utils.convertJSONToResponse(
163 proxyInstance, result.modifiedResponse);
164 proxyInstance.$newResponseText = utils.convertJSONToResponseText(
165 proxyInstance, result.modifiedResponse);
166 }
167
168 proxyInstance.$responseModified = result.wasModified;
169 proxyInstance.$responseIntercepted = true;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100170 } else {
Adrià Vilanova Martínez47c4c812024-12-05 15:34:40 +0100171 await waitFor(() => {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100172 if (proxyInstance.$responseIntercepted) return Promise.resolve();
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100173 return Promise.reject();
174 }, kCheckInterceptionOptions);
175 }
176
Adrià Vilanova Martínez47c4c812024-12-05 15:34:40 +0100177 for (const e of proxyInstance.specialHandlers[eventName]) {
178 e[1](arguments);
179 }
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100180 });
181 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100182 };
183 }
184
185 #addProxyErrorEvents() {
186 window.XMLHttpRequest.prototype.$proxyErrorEvents = function() {
187 const proxyInstance = this;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100188 kErrorEvents.forEach(eventName => {
189 this.xhr.addEventListener(eventName, function() {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100190 proxyInstance.$responseIntercepted = true;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100191 });
192 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100193 }
194 }
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100195
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100196 #overrideStandardMethod(method) {
197 window.XMLHttpRequest.prototype[method] = function() {
198 if (method == 'open')
199 this.$interceptOpen(...arguments);
200 else if (method == 'setRequestHeader')
201 this.$interceptSetRequestHeader(...arguments);
202 else if (method == 'send')
203 this.$interceptSend(...arguments);
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100204
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100205 return this.xhr[method].apply(this.xhr, arguments);
206 }
207 }
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100208
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100209 #overrideStandardScalar(scalar) {
210 Object.defineProperty(window.XMLHttpRequest.prototype, scalar, {
211 get: function() {
212 return this.xhr[scalar];
213 },
214 set: function(val) {
215 this.xhr[scalar] = val;
216 },
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200217 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100218 }
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200219
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100220 #overrideAddEventListener() {
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100221 window.XMLHttpRequest.prototype.addEventListener = function() {
222 if (!kSpecialEvents.includes(arguments[0]))
223 return this.xhr.addEventListener.apply(this.xhr, arguments);
224
225 this.specialHandlers[arguments[0]].add(arguments);
226 };
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100227 }
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100228
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100229 #overrideRemoveEventListener() {
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100230 window.XMLHttpRequest.prototype.removeEventListener = function(
231 type, callback, options) {
232 if (!kSpecialEvents.includes(type))
233 return this.xhr.removeEventListener.apply(this.xhr, arguments);
234
235 const flattenedOptions = flattenOptions(options);
236 for (const e of this.specialHandlers[type]) {
237 if (callback === e[1] && flattenOptions(e[2]) === flattenedOptions) {
238 return this.specialHandlers[type].delete(e);
239 }
240 }
241 };
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100242 }
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100243
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100244 #addOpenInterceptor() {
245 window.XMLHttpRequest.prototype.$interceptOpen = function() {
246 const proxyInstance = this;
247
248 this.$TWPTRequestURL = arguments[1] || location.href;
249
250 var interceptors =
251 utils.matchInterceptors('response', this.$TWPTRequestURL);
252 if (interceptors.length > 0) {
Adrià Vilanova Martínez88e5ef22023-11-15 22:45:13 +0100253 this.addEventListener('load', function() {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100254 var body = utils.getResponseJSON({
Adrià Vilanova Martínez88e5ef22023-11-15 22:45:13 +0100255 responseType: proxyInstance.responseType,
256 response: proxyInstance.response,
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100257 $TWPTRequestURL: proxyInstance.$TWPTRequestURL,
258 $isArrayProto: proxyInstance.$isArrayProto,
259 });
260 if (body !== undefined)
261 interceptors.forEach(i => {
262 utils.triggerEvent(i.eventName, body, proxyInstance.$TWPTID);
263 });
264 });
265 }
266 };
267 }
268
269 #addSetRequestHeaderInterceptor() {
270 window.XMLHttpRequest.prototype.$interceptSetRequestHeader = function() {
271 let header = arguments[0];
272 let value = arguments[1];
273 if ('Content-Type'.localeCompare(
274 header, undefined, {sensitivity: 'accent'}) == 0)
275 this.$isArrayProto = (value == 'application/json+protobuf');
276 };
277 }
278
279 #addSendInterceptor() {
280 window.XMLHttpRequest.prototype.$interceptSend = function() {
281 var interceptors = utils.matchInterceptors(
282 'request', this.$TWPTRequestURL || location.href);
283 if (interceptors.length > 0) {
284 let rawBody = arguments[0];
285 let body;
286 if (typeof (rawBody) === 'object' &&
287 (rawBody instanceof Object.getPrototypeOf(Uint8Array))) {
288 let dec = new TextDecoder('utf-8');
289 body = dec.decode(rawBody);
290 } else if (typeof (rawBody) === 'string') {
291 body = rawBody;
292 } else {
293 console.error(
294 'Unexpected type of request body (' + typeof (rawBody) + ').',
295 this.$TWPTRequestURL);
296 return;
297 }
298
299 let JSONBody = JSON.parse(body);
300 if (this.$isArrayProto) JSONBody = correctArrayKeys(JSONBody);
301
302 interceptors.forEach(i => {
303 utils.triggerEvent(i.eventName, JSONBody, this.$TWPTID);
304 });
305 }
306 }
307 }
308
309 #setOriginalResponseScalar() {
310 Object.defineProperty(window.XMLHttpRequest.prototype, 'originalResponse', {
311 get: function() {
312 return this.xhr.response;
313 },
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200314 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100315 }
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100316
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100317 #overrideResponse() {
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100318 Object.defineProperty(window.XMLHttpRequest.prototype, 'response', {
319 get: function() {
320 if (!this.$responseIntercepted) return undefined;
321 if (this.$responseModified) return this.$newResponse;
322 return this.xhr.response;
323 },
324 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100325 }
326
327 #overrideResponseText() {
Adrià Vilanova Martínez8a17fa82023-02-04 19:19:27 +0100328 Object.defineProperty(window.XMLHttpRequest.prototype, 'responseText', {
329 get: function() {
330 if (!this.$responseIntercepted) return undefined;
331 if (this.$responseModified) return this.$newResponseText;
332 return this.xhr.responseText;
333 },
334 });
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200335 }
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100336}