blob: 1552966d082af778820bbc0cbae8a92a49ad63a5 [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ínez102d54b2022-12-18 11:12:11 +01004import * as utils from '../xhrInterceptor/utils.js';
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +02005
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +01006const kSpecialEvents = ['load', 'loadend'];
7const kErrorEvents = ['error', 'timeout', 'abort'];
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +02008
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +01009const kStandardMethods = [
10 'open', 'abort', 'setRequestHeader', 'send', 'getResponseHeader',
11 'getAllResponseHeaders', 'dispatchEvent', 'overrideMimeType'
12];
13const kStandardScalars = [
Adrià Vilanova Martínez48dbe7c2024-02-26 04:40:34 +010014 'onabort',
15 'onerror',
16 'onload',
17 'onloadstart',
18 'onloadend',
19 'onprogress',
20 'onreadystatechange',
21 'readyState',
22 'responseType',
23 'responseURL',
24 'responseXML',
25 'status',
26 'statusText',
27 'upload',
28 'withCredentials',
29 'DONE',
30 'UNSENT',
31 'HEADERS_RECEIVED',
32 'LOADING',
33 'OPENED',
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010034];
35
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010036const kCheckInterceptionOptions = {
37 interval: 50,
38 timeout: 100 * 1000,
39};
avm999631f50f6f2021-08-12 23:04:41 +020040
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010041/**
42 * Implements the flatten options concept of the DOM spec.
43 *
44 * @see {@link https://dom.spec.whatwg.org/#concept-flatten-options}
45 */
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010046function flattenOptions(options) {
47 if (typeof options === 'boolean') return options;
48 if (options) return options['capture'];
49 return undefined;
50}
51
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010052/**
53 * Class which, when instantiated, overrides window.XMLHttpRequest to proxy the
54 * requests through our internal interceptors to read/modify requests/responses.
55 *
56 * Slightly based in https://stackoverflow.com/a/24561614.
Adrià Vilanova Martínez9c418ab2024-12-05 15:34:40 +010057 *
58 * @param responseModifier
59 * @param messageIdTracker
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010060 */
Adrià Vilanova Martínez4757ae82022-12-28 16:41:37 +010061export default class XHRProxy {
Adrià Vilanova Martínez9c418ab2024-12-05 15:34:40 +010062 constructor(responseModifier, messageIdTracker) {
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010063 this.originalXMLHttpRequest = window.XMLHttpRequest;
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +020064
Adrià Vilanova Martínez9c418ab2024-12-05 15:34:40 +010065 this.messageIdTracker = messageIdTracker;
66 this.responseModifier = responseModifier;
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010067
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010068 this.#overrideXHRObject();
69 }
70
71 #overrideXHRObject() {
72 this.#overrideConstructor();
73 this.#addProxyEventsMethods();
74 this.#overrideMethods();
75 this.#addMethodInterceptors();
76 this.#overrideScalars();
77 this.#setOriginalResponseScalar();
78 }
79
80 #overrideConstructor() {
81 const XHRProxyInstance = this;
82
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010083 window.XMLHttpRequest = function() {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010084 this.xhr = new XHRProxyInstance.originalXMLHttpRequest();
Adrià Vilanova Martínez9c418ab2024-12-05 15:34:40 +010085 this.$TWPTID = XHRProxyInstance.messageIdTracker.getNewId();
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010086 this.$responseModified = false;
87 this.$responseIntercepted = false;
88 this.specialHandlers = {
89 load: new Set(),
90 loadend: new Set(),
91 };
92
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010093 this.$proxyEvents();
94 };
95 }
96
97 #addProxyEventsMethods() {
98 window.XMLHttpRequest.prototype.$proxyEvents = function() {
99 this.$proxySpecialEvents();
100 this.$proxyErrorEvents();
101 };
102 this.#addProxySpecialEvents();
103 this.#addProxyErrorEvents();
104 }
105
106 #overrideMethods() {
107 this.#overrideStandardMethods();
108 this.#overrideEventsMethods();
109 }
110
111 #addMethodInterceptors() {
112 this.#addOpenInterceptor();
113 this.#addSetRequestHeaderInterceptor();
114 this.#addSendInterceptor();
115 }
116
117 #overrideScalars() {
118 this.#overrideStandardScalars();
119 this.#overrideResponseScalars();
120 }
121
122 #overrideStandardMethods() {
123 kStandardMethods.forEach(method => {
124 this.#overrideStandardMethod(method);
125 });
126 }
127
128 #overrideEventsMethods() {
129 this.#overrideAddEventListener();
130 this.#overrideRemoveEventListener();
131 }
132
133 #overrideStandardScalars() {
134 kStandardScalars.forEach(scalar => {
135 this.#overrideStandardScalar(scalar);
136 });
137 }
138
139 #overrideResponseScalars() {
140 this.#overrideResponse();
141 this.#overrideResponseText();
142 }
143
144 #addProxySpecialEvents() {
145 const XHRProxyInstance = this;
146 window.XMLHttpRequest.prototype.$proxySpecialEvents = function() {
147 const proxyInstance = this;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100148 kSpecialEvents.forEach(eventName => {
Adrià Vilanova Martínez47c4c812024-12-05 15:34:40 +0100149 this.xhr.addEventListener(eventName, async function() {
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100150 if (eventName === 'load') {
Adrià Vilanova Martínez47c4c812024-12-05 15:34:40 +0100151 const initialResponse = utils.getResponseJSON({
152 responseType: proxyInstance.xhr.responseType,
153 response: proxyInstance.xhr.response,
154 $TWPTRequestURL: proxyInstance.$TWPTRequestURL,
155 $isArrayProto: proxyInstance.$isArrayProto,
156 });
157
158 const result = await XHRProxyInstance.responseModifier.intercept({
159 url: proxyInstance.$TWPTRequestURL,
160 originalResponse: initialResponse,
161 });
162
163 if (result.wasModified) {
164 proxyInstance.$newResponse = utils.convertJSONToResponse(
165 proxyInstance, result.modifiedResponse);
166 proxyInstance.$newResponseText = utils.convertJSONToResponseText(
167 proxyInstance, result.modifiedResponse);
168 }
169
170 proxyInstance.$responseModified = result.wasModified;
171 proxyInstance.$responseIntercepted = true;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100172 } else {
Adrià Vilanova Martínez47c4c812024-12-05 15:34:40 +0100173 await waitFor(() => {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100174 if (proxyInstance.$responseIntercepted) return Promise.resolve();
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100175 return Promise.reject();
176 }, kCheckInterceptionOptions);
177 }
178
Adrià Vilanova Martínez47c4c812024-12-05 15:34:40 +0100179 for (const e of proxyInstance.specialHandlers[eventName]) {
180 e[1](arguments);
181 }
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100182 });
183 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100184 };
185 }
186
187 #addProxyErrorEvents() {
188 window.XMLHttpRequest.prototype.$proxyErrorEvents = function() {
189 const proxyInstance = this;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100190 kErrorEvents.forEach(eventName => {
191 this.xhr.addEventListener(eventName, function() {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100192 proxyInstance.$responseIntercepted = true;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100193 });
194 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100195 }
196 }
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100197
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100198 #overrideStandardMethod(method) {
199 window.XMLHttpRequest.prototype[method] = function() {
200 if (method == 'open')
201 this.$interceptOpen(...arguments);
202 else if (method == 'setRequestHeader')
203 this.$interceptSetRequestHeader(...arguments);
204 else if (method == 'send')
205 this.$interceptSend(...arguments);
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100206
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100207 return this.xhr[method].apply(this.xhr, arguments);
208 }
209 }
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100210
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100211 #overrideStandardScalar(scalar) {
212 Object.defineProperty(window.XMLHttpRequest.prototype, scalar, {
213 get: function() {
214 return this.xhr[scalar];
215 },
216 set: function(val) {
217 this.xhr[scalar] = val;
218 },
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200219 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100220 }
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200221
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100222 #overrideAddEventListener() {
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100223 window.XMLHttpRequest.prototype.addEventListener = function() {
224 if (!kSpecialEvents.includes(arguments[0]))
225 return this.xhr.addEventListener.apply(this.xhr, arguments);
226
227 this.specialHandlers[arguments[0]].add(arguments);
228 };
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100229 }
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100230
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100231 #overrideRemoveEventListener() {
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100232 window.XMLHttpRequest.prototype.removeEventListener = function(
233 type, callback, options) {
234 if (!kSpecialEvents.includes(type))
235 return this.xhr.removeEventListener.apply(this.xhr, arguments);
236
237 const flattenedOptions = flattenOptions(options);
238 for (const e of this.specialHandlers[type]) {
239 if (callback === e[1] && flattenOptions(e[2]) === flattenedOptions) {
240 return this.specialHandlers[type].delete(e);
241 }
242 }
243 };
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100244 }
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100245
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100246 #addOpenInterceptor() {
247 window.XMLHttpRequest.prototype.$interceptOpen = function() {
248 const proxyInstance = this;
249
250 this.$TWPTRequestURL = arguments[1] || location.href;
251
252 var interceptors =
253 utils.matchInterceptors('response', this.$TWPTRequestURL);
254 if (interceptors.length > 0) {
Adrià Vilanova Martínez88e5ef22023-11-15 22:45:13 +0100255 this.addEventListener('load', function() {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100256 var body = utils.getResponseJSON({
Adrià Vilanova Martínez88e5ef22023-11-15 22:45:13 +0100257 responseType: proxyInstance.responseType,
258 response: proxyInstance.response,
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100259 $TWPTRequestURL: proxyInstance.$TWPTRequestURL,
260 $isArrayProto: proxyInstance.$isArrayProto,
261 });
262 if (body !== undefined)
263 interceptors.forEach(i => {
264 utils.triggerEvent(i.eventName, body, proxyInstance.$TWPTID);
265 });
266 });
267 }
268 };
269 }
270
271 #addSetRequestHeaderInterceptor() {
272 window.XMLHttpRequest.prototype.$interceptSetRequestHeader = function() {
273 let header = arguments[0];
274 let value = arguments[1];
275 if ('Content-Type'.localeCompare(
276 header, undefined, {sensitivity: 'accent'}) == 0)
277 this.$isArrayProto = (value == 'application/json+protobuf');
278 };
279 }
280
281 #addSendInterceptor() {
282 window.XMLHttpRequest.prototype.$interceptSend = function() {
283 var interceptors = utils.matchInterceptors(
284 'request', this.$TWPTRequestURL || location.href);
285 if (interceptors.length > 0) {
286 let rawBody = arguments[0];
287 let body;
288 if (typeof (rawBody) === 'object' &&
289 (rawBody instanceof Object.getPrototypeOf(Uint8Array))) {
290 let dec = new TextDecoder('utf-8');
291 body = dec.decode(rawBody);
292 } else if (typeof (rawBody) === 'string') {
293 body = rawBody;
294 } else {
295 console.error(
296 'Unexpected type of request body (' + typeof (rawBody) + ').',
297 this.$TWPTRequestURL);
298 return;
299 }
300
301 let JSONBody = JSON.parse(body);
302 if (this.$isArrayProto) JSONBody = correctArrayKeys(JSONBody);
303
304 interceptors.forEach(i => {
305 utils.triggerEvent(i.eventName, JSONBody, this.$TWPTID);
306 });
307 }
308 }
309 }
310
311 #setOriginalResponseScalar() {
312 Object.defineProperty(window.XMLHttpRequest.prototype, 'originalResponse', {
313 get: function() {
314 return this.xhr.response;
315 },
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200316 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100317 }
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100318
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100319 #overrideResponse() {
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100320 Object.defineProperty(window.XMLHttpRequest.prototype, 'response', {
321 get: function() {
322 if (!this.$responseIntercepted) return undefined;
323 if (this.$responseModified) return this.$newResponse;
324 return this.xhr.response;
325 },
326 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100327 }
328
329 #overrideResponseText() {
Adrià Vilanova Martínez8a17fa82023-02-04 19:19:27 +0100330 Object.defineProperty(window.XMLHttpRequest.prototype, 'responseText', {
331 get: function() {
332 if (!this.$responseIntercepted) return undefined;
333 if (this.$responseModified) return this.$newResponseText;
334 return this.xhr.responseText;
335 },
336 });
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200337 }
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100338}