-
Notifications
You must be signed in to change notification settings - Fork 2k
/
Copy pathproxySandbox.ts
431 lines (367 loc) · 17.1 KB
/
proxySandbox.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
/* eslint-disable no-param-reassign */
/**
* @author Kuitos
* @since 2020-3-31
*/
import { without } from 'lodash';
import type { SandBox } from '../interfaces';
import { SandBoxType } from '../interfaces';
import { isPropertyFrozen, nativeGlobal, nextTask } from '../utils';
import { clearCurrentRunningApp, getCurrentRunningApp, rebindTarget2Fn, setCurrentRunningApp } from './common';
import { globalsInBrowser, globalsInES2015 } from './globals';
type SymbolTarget = 'target' | 'globalContext';
type FakeWindow = Window & Record<PropertyKey, any>;
/**
* fastest(at most time) unique array method
* @see https://jsperf.com/array-filter-unique/30
*/
function uniq(array: Array<string | symbol>) {
return array.filter(function filter(this: PropertyKey[], element) {
return element in this ? false : ((this as any)[element] = true);
}, Object.create(null));
}
/**
* transform array to object to enable faster element check with in operator
* @param array
*/
function array2TruthyObject(array: string[]): Record<string, true> {
return array.reduce(
(acc, key) => {
acc[key] = true;
return acc;
},
// Notes that babel will transpile spread operator to Object.assign({}, ...args), which will keep the prototype of Object in merged object,
// while this result used as Symbol.unscopables, it will make properties in Object.prototype always be escaped from proxy sandbox as unscopables check will look up prototype chain as well,
// such as hasOwnProperty, toString, valueOf, etc.
// so we should use Object.create(null) to create a pure object without prototype chain here.
Object.create(null),
);
}
const cachedGlobalsInBrowser = array2TruthyObject(
globalsInBrowser.concat(process.env.NODE_ENV === 'test' ? ['mockNativeWindowFunction'] : []),
);
function isNativeGlobalProp(prop: string): boolean {
return prop in cachedGlobalsInBrowser;
}
// zone.js will overwrite Object.defineProperty
const rawObjectDefineProperty = Object.defineProperty;
const variableWhiteListInDev =
process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'development' || window.__QIANKUN_DEVELOPMENT__
? [
// for react hot reload
// see https://github.com/facebook/create-react-app/blob/66bf7dfc43350249e2f09d138a20840dae8a0a4a/packages/react-error-overlay/src/index.js#L180
'__REACT_ERROR_OVERLAY_GLOBAL_HOOK__',
// for react development event issue, see https://github.com/umijs/qiankun/issues/2375
'event',
]
: [];
// who could escape the sandbox
const globalVariableWhiteList: string[] = [
// FIXME System.js used a indirect call with eval, which would make it scope escape to global
// To make System.js works well, we write it back to global window temporary
// see https://github.com/systemjs/systemjs/blob/457f5b7e8af6bd120a279540477552a07d5de086/src/evaluate.js#L106
'System',
// see https://github.com/systemjs/systemjs/blob/457f5b7e8af6bd120a279540477552a07d5de086/src/instantiate.js#L357
'__cjsWrapper',
...variableWhiteListInDev,
];
const inTest = process.env.NODE_ENV === 'test';
const mockSafariTop = 'mockSafariTop';
const mockTop = 'mockTop';
const mockGlobalThis = 'mockGlobalThis';
// these globals should be recorded while accessing every time
const accessingSpiedGlobals = ['document', 'top', 'parent', 'eval'];
const overwrittenGlobals = ['window', 'self', 'globalThis', 'hasOwnProperty'].concat(inTest ? [mockGlobalThis] : []);
export const cachedGlobals = Array.from(
new Set(
without(globalsInES2015.concat(overwrittenGlobals).concat('requestAnimationFrame'), ...accessingSpiedGlobals),
),
);
const cachedGlobalObjects = array2TruthyObject(cachedGlobals);
/*
Variables who are impossible to be overwritten need to be escaped from proxy sandbox for performance reasons.
But overwritten globals must not be escaped, otherwise they will be leaked to the global scope.
see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/unscopables
*/
const unscopables = array2TruthyObject(without(cachedGlobals, ...accessingSpiedGlobals.concat(overwrittenGlobals)));
const useNativeWindowForBindingsProps = new Map<PropertyKey, boolean>([
['fetch', true],
['mockDomAPIInBlackList', process.env.NODE_ENV === 'test'],
]);
function createFakeWindow(globalContext: Window, speedy: boolean) {
// map always has the fastest performance in has checked scenario
// see https://jsperf.com/array-indexof-vs-set-has/23
const propertiesWithGetter = new Map<PropertyKey, boolean>();
const fakeWindow = {} as FakeWindow;
/*
copy the non-configurable property of global to fakeWindow
see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor
> A property cannot be reported as non-configurable, if it does not exist as an own property of the target object or if it exists as a configurable own property of the target object.
*/
Object.getOwnPropertyNames(globalContext)
.filter((p) => {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
return !descriptor?.configurable;
})
.forEach((p) => {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
if (descriptor) {
const hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get');
/*
make top/self/window property configurable and writable, otherwise it will cause TypeError while get trap return.
see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get
> The value reported for a property must be the same as the value of the corresponding target object property if the target object property is a non-writable, non-configurable data property.
*/
if (
p === 'top' ||
p === 'parent' ||
p === 'self' ||
p === 'window' ||
// window.document is overwriting in speedy mode
(p === 'document' && speedy) ||
(inTest && (p === mockTop || p === mockSafariTop))
) {
descriptor.configurable = true;
/*
The descriptor of window.window/window.top/window.self in Safari/FF are accessor descriptors, we need to avoid adding a data descriptor while it was
Example:
Safari/FF: Object.getOwnPropertyDescriptor(window, 'top') -> {get: function, set: undefined, enumerable: true, configurable: false}
Chrome: Object.getOwnPropertyDescriptor(window, 'top') -> {value: Window, writable: false, enumerable: true, configurable: false}
*/
if (!hasGetter) {
descriptor.writable = true;
}
}
if (hasGetter) propertiesWithGetter.set(p, true);
// freeze the descriptor to avoid being modified by zone.js
// see https://github.com/angular/zone.js/blob/a5fe09b0fac27ac5df1fa746042f96f05ccb6a00/lib/browser/define-property.ts#L71
rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor));
}
});
return {
fakeWindow,
propertiesWithGetter,
};
}
let activeSandboxCount = 0;
/**
* 基于 Proxy 实现的沙箱
*/
export default class ProxySandbox implements SandBox {
/** window 值变更记录 */
private updatedValueSet = new Set<PropertyKey>();
private document = document;
name: string;
type: SandBoxType;
proxy: WindowProxy;
sandboxRunning = true;
latestSetProp: PropertyKey | null = null;
active() {
if (!this.sandboxRunning) activeSandboxCount++;
this.sandboxRunning = true;
}
inactive() {
if (process.env.NODE_ENV === 'development') {
console.info(`[qiankun:sandbox] ${this.name} modified global properties restore...`, [
...this.updatedValueSet.keys(),
]);
}
if (inTest || --activeSandboxCount === 0) {
// reset the global value to the prev value
Object.keys(this.globalWhitelistPrevDescriptor).forEach((p) => {
const descriptor = this.globalWhitelistPrevDescriptor[p];
if (descriptor) {
Object.defineProperty(this.globalContext, p, descriptor);
} else {
// @ts-ignore
delete this.globalContext[p];
}
});
}
this.sandboxRunning = false;
}
public patchDocument(doc: Document) {
this.document = doc;
}
// the descriptor of global variables in whitelist before it been modified
globalWhitelistPrevDescriptor: { [p in (typeof globalVariableWhiteList)[number]]: PropertyDescriptor | undefined } =
{};
globalContext: typeof window;
constructor(name: string, globalContext = window, opts?: { speedy: boolean }) {
this.name = name;
this.globalContext = globalContext;
this.type = SandBoxType.Proxy;
const { updatedValueSet } = this;
const { speedy } = opts || {};
const { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext, !!speedy);
const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>();
const proxy = new Proxy(fakeWindow, {
set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
if (this.sandboxRunning) {
this.registerRunningApp(name, proxy);
// sync the property to globalContext
if (typeof p === 'string' && globalVariableWhiteList.indexOf(p) !== -1) {
this.globalWhitelistPrevDescriptor[p] = Object.getOwnPropertyDescriptor(globalContext, p);
// @ts-ignore
globalContext[p] = value;
} else {
// We must keep its description while the property existed in globalContext before
if (!target.hasOwnProperty(p) && globalContext.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
const { writable, configurable, enumerable, set } = descriptor!;
// only writable property can be overwritten
// here we ignored accessor descriptor of globalContext as it makes no sense to trigger its logic(which might make sandbox escaping instead)
// we force to set value by data descriptor
if (writable || set) {
Object.defineProperty(target, p, { configurable, enumerable, writable: true, value });
}
} else {
target[p] = value;
}
}
updatedValueSet.add(p);
this.latestSetProp = p;
return true;
}
if (process.env.NODE_ENV === 'development') {
console.warn(`[qiankun] Set window.${p.toString()} while sandbox destroyed or inactive in ${name}!`);
}
// 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
return true;
},
get: (target: FakeWindow, p: PropertyKey): any => {
this.registerRunningApp(name, proxy);
if (p === Symbol.unscopables) return unscopables;
// avoid who using window.window or window.self to escape the sandbox environment to touch the real window
// see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13
if (p === 'window' || p === 'self') {
return proxy;
}
// hijack globalWindow accessing with globalThis keyword
if (p === 'globalThis' || (inTest && p === mockGlobalThis)) {
return proxy;
}
if (p === 'top' || p === 'parent' || (inTest && (p === mockTop || p === mockSafariTop))) {
// if your master app in an iframe context, allow these props escape the sandbox
if (globalContext === globalContext.parent) {
return proxy;
}
return (globalContext as any)[p];
}
// proxy.hasOwnProperty would invoke getter firstly, then its value represented as globalContext.hasOwnProperty
if (p === 'hasOwnProperty') {
return hasOwnProperty;
}
if (p === 'document') {
return this.document;
}
if (p === 'eval') {
return eval;
}
if (p === 'string' && globalVariableWhiteList.indexOf(p) !== -1) {
// @ts-ignore
return globalContext[p];
}
const actualTarget = propertiesWithGetter.has(p) ? globalContext : p in target ? target : globalContext;
const value = actualTarget[p];
// frozen value should return directly, see https://github.com/umijs/qiankun/issues/2015
if (isPropertyFrozen(actualTarget, p)) {
return value;
}
// non-native property return directly to avoid rebind
if (!isNativeGlobalProp(p as string) && !useNativeWindowForBindingsProps.has(p)) {
return value;
}
/* Some dom api must be bound to native window, otherwise it would cause exception like 'TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation'
See this code:
const proxy = new Proxy(window, {});
// in nest sandbox fetch will be bind to proxy rather than window in master
const proxyFetch = fetch.bind(proxy);
proxyFetch('https://qiankun.com');
*/
const boundTarget = useNativeWindowForBindingsProps.get(p) ? nativeGlobal : globalContext;
return rebindTarget2Fn(boundTarget, value);
},
// trap in operator
// see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12
has(target: FakeWindow, p: string | number | symbol): boolean {
// property in cachedGlobalObjects must return true to avoid escape from get trap
return p in cachedGlobalObjects || p in target || p in globalContext;
},
getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined {
/*
as the descriptor of top/self/window/mockTop in raw window are configurable but not in proxy target, we need to get it from target to avoid TypeError
see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor
> A property cannot be reported as non-configurable, if it does not exist as an own property of the target object or if it exists as a configurable own property of the target object.
*/
if (target.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(target, p);
descriptorTargetMap.set(p, 'target');
return descriptor;
}
if (globalContext.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
descriptorTargetMap.set(p, 'globalContext');
// A property cannot be reported as non-configurable, if it does not exist as an own property of the target object
if (descriptor && !descriptor.configurable) {
descriptor.configurable = true;
}
return descriptor;
}
return undefined;
},
// trap to support iterator with sandbox
ownKeys(target: FakeWindow): ArrayLike<string | symbol> {
return uniq(Reflect.ownKeys(globalContext).concat(Reflect.ownKeys(target)));
},
defineProperty: (target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean => {
const from = descriptorTargetMap.get(p);
/*
Descriptor must be defined to native window while it comes from native window via Object.getOwnPropertyDescriptor(window, p),
otherwise it would cause a TypeError with illegal invocation.
*/
switch (from) {
case 'globalContext':
return Reflect.defineProperty(globalContext, p, attributes);
default:
return Reflect.defineProperty(target, p, attributes);
}
},
deleteProperty: (target: FakeWindow, p: string | number | symbol): boolean => {
this.registerRunningApp(name, proxy);
if (target.hasOwnProperty(p)) {
// @ts-ignore
delete target[p];
updatedValueSet.delete(p);
return true;
}
return true;
},
// makes sure `window instanceof Window` returns truthy in micro app
getPrototypeOf() {
return Reflect.getPrototypeOf(globalContext);
},
});
this.proxy = proxy;
activeSandboxCount++;
function hasOwnProperty(this: any, key: PropertyKey): boolean {
// calling from hasOwnProperty.call(obj, key)
if (this !== proxy && this !== null && typeof this === 'object') {
return Object.prototype.hasOwnProperty.call(this, key);
}
return fakeWindow.hasOwnProperty(key) || globalContext.hasOwnProperty(key);
}
}
private registerRunningApp(name: string, proxy: Window) {
if (this.sandboxRunning) {
const currentRunningApp = getCurrentRunningApp();
if (!currentRunningApp || currentRunningApp.name !== name) {
setCurrentRunningApp({ name, window: proxy });
}
// FIXME if you have any other good ideas
// remove the mark in next tick, thus we can identify whether it in micro app or not
// this approach is just a workaround, it could not cover all complex cases, such as the micro app runs in the same task context with master in some case
nextTask(clearCurrentRunningApp);
}
}
}