crucible 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +102 -0
  4. data/Gemfile +10 -0
  5. data/LICENSE +21 -0
  6. data/README.md +366 -0
  7. data/Rakefile +23 -0
  8. data/TESTING.md +319 -0
  9. data/config.sample.yml +48 -0
  10. data/crucible.gemspec +48 -0
  11. data/exe/crucible +122 -0
  12. data/lib/crucible/configuration.rb +212 -0
  13. data/lib/crucible/server.rb +123 -0
  14. data/lib/crucible/session_manager.rb +209 -0
  15. data/lib/crucible/stealth/evasions/chrome_app.js +75 -0
  16. data/lib/crucible/stealth/evasions/chrome_csi.js +33 -0
  17. data/lib/crucible/stealth/evasions/chrome_load_times.js +44 -0
  18. data/lib/crucible/stealth/evasions/chrome_runtime.js +190 -0
  19. data/lib/crucible/stealth/evasions/iframe_content_window.js +101 -0
  20. data/lib/crucible/stealth/evasions/media_codecs.js +65 -0
  21. data/lib/crucible/stealth/evasions/navigator_hardware_concurrency.js +18 -0
  22. data/lib/crucible/stealth/evasions/navigator_languages.js +18 -0
  23. data/lib/crucible/stealth/evasions/navigator_permissions.js +53 -0
  24. data/lib/crucible/stealth/evasions/navigator_plugins.js +261 -0
  25. data/lib/crucible/stealth/evasions/navigator_vendor.js +18 -0
  26. data/lib/crucible/stealth/evasions/navigator_webdriver.js +16 -0
  27. data/lib/crucible/stealth/evasions/webgl_vendor.js +43 -0
  28. data/lib/crucible/stealth/evasions/window_outerdimensions.js +18 -0
  29. data/lib/crucible/stealth/utils.js +266 -0
  30. data/lib/crucible/stealth.rb +213 -0
  31. data/lib/crucible/tools/cookies.rb +206 -0
  32. data/lib/crucible/tools/downloads.rb +273 -0
  33. data/lib/crucible/tools/extraction.rb +335 -0
  34. data/lib/crucible/tools/helpers.rb +46 -0
  35. data/lib/crucible/tools/interaction.rb +355 -0
  36. data/lib/crucible/tools/navigation.rb +181 -0
  37. data/lib/crucible/tools/sessions.rb +85 -0
  38. data/lib/crucible/tools/stealth.rb +167 -0
  39. data/lib/crucible/tools.rb +42 -0
  40. data/lib/crucible/version.rb +5 -0
  41. data/lib/crucible.rb +60 -0
  42. metadata +201 -0
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Evasion: chrome.runtime
3
+ * Mock the chrome.runtime object on secure origins.
4
+ */
5
+ (function() {
6
+ 'use strict';
7
+
8
+ const utils = window._stealthUtils;
9
+ if (!utils) return;
10
+
11
+ if (!window.chrome) {
12
+ Object.defineProperty(window, 'chrome', {
13
+ writable: true,
14
+ enumerable: true,
15
+ configurable: false,
16
+ value: {}
17
+ });
18
+ }
19
+
20
+ const existsAlready = 'runtime' in window.chrome;
21
+ const isNotSecure = !window.location.protocol.startsWith('https');
22
+ if (existsAlready || isNotSecure) {
23
+ return;
24
+ }
25
+
26
+ const STATIC_DATA = {
27
+ OnInstalledReason: {
28
+ CHROME_UPDATE: 'chrome_update',
29
+ INSTALL: 'install',
30
+ SHARED_MODULE_UPDATE: 'shared_module_update',
31
+ UPDATE: 'update'
32
+ },
33
+ OnRestartRequiredReason: {
34
+ APP_UPDATE: 'app_update',
35
+ OS_UPDATE: 'os_update',
36
+ PERIODIC: 'periodic'
37
+ },
38
+ PlatformArch: {
39
+ ARM: 'arm',
40
+ ARM64: 'arm64',
41
+ MIPS: 'mips',
42
+ MIPS64: 'mips64',
43
+ X86_32: 'x86-32',
44
+ X86_64: 'x86-64'
45
+ },
46
+ PlatformNaclArch: {
47
+ ARM: 'arm',
48
+ MIPS: 'mips',
49
+ MIPS64: 'mips64',
50
+ X86_32: 'x86-32',
51
+ X86_64: 'x86-64'
52
+ },
53
+ PlatformOs: {
54
+ ANDROID: 'android',
55
+ CROS: 'cros',
56
+ LINUX: 'linux',
57
+ MAC: 'mac',
58
+ OPENBSD: 'openbsd',
59
+ WIN: 'win'
60
+ },
61
+ RequestUpdateCheckStatus: {
62
+ NO_UPDATE: 'no_update',
63
+ THROTTLED: 'throttled',
64
+ UPDATE_AVAILABLE: 'update_available'
65
+ }
66
+ };
67
+
68
+ window.chrome.runtime = {
69
+ ...STATIC_DATA,
70
+ get id() {
71
+ return undefined;
72
+ },
73
+ connect: null,
74
+ sendMessage: null
75
+ };
76
+
77
+ const makeCustomRuntimeErrors = (preamble, method, extensionId) => ({
78
+ NoMatchingSignature: new TypeError(preamble + 'No matching signature.'),
79
+ MustSpecifyExtensionID: new TypeError(
80
+ preamble + `${method} called from a webpage must specify an Extension ID (string) for its first argument.`
81
+ ),
82
+ InvalidExtensionID: new TypeError(preamble + `Invalid extension id: '${extensionId}'`)
83
+ });
84
+
85
+ const isValidExtensionID = str =>
86
+ str.length === 32 && str.toLowerCase().match(/^[a-p]+$/);
87
+
88
+ // Mock sendMessage
89
+ const sendMessageHandler = {
90
+ apply: function(target, ctx, args) {
91
+ const [extensionId, options, responseCallback] = args || [];
92
+ const errorPreamble = 'Error in invocation of runtime.sendMessage(optional string extensionId, any message, optional object options, optional function responseCallback): ';
93
+ const Errors = makeCustomRuntimeErrors(errorPreamble, 'chrome.runtime.sendMessage()', extensionId);
94
+
95
+ const noArguments = args.length === 0;
96
+ const tooManyArguments = args.length > 4;
97
+ const incorrectOptions = options && typeof options !== 'object';
98
+ const incorrectResponseCallback = responseCallback && typeof responseCallback !== 'function';
99
+
100
+ if (noArguments || tooManyArguments || incorrectOptions || incorrectResponseCallback) {
101
+ throw Errors.NoMatchingSignature;
102
+ }
103
+ if (args.length < 2) {
104
+ throw Errors.MustSpecifyExtensionID;
105
+ }
106
+ if (typeof extensionId !== 'string') {
107
+ throw Errors.NoMatchingSignature;
108
+ }
109
+ if (!isValidExtensionID(extensionId)) {
110
+ throw Errors.InvalidExtensionID;
111
+ }
112
+ return undefined;
113
+ }
114
+ };
115
+
116
+ utils.mockWithProxy(
117
+ window.chrome.runtime,
118
+ 'sendMessage',
119
+ function sendMessage() {},
120
+ sendMessageHandler
121
+ );
122
+
123
+ // Mock connect
124
+ const connectHandler = {
125
+ apply: function(target, ctx, args) {
126
+ const [extensionId, connectInfo] = args || [];
127
+ const errorPreamble = 'Error in invocation of runtime.connect(optional string extensionId, optional object connectInfo): ';
128
+ const Errors = makeCustomRuntimeErrors(errorPreamble, 'chrome.runtime.connect()', extensionId);
129
+
130
+ const noArguments = args.length === 0;
131
+ const emptyStringArgument = args.length === 1 && extensionId === '';
132
+ if (noArguments || emptyStringArgument) {
133
+ throw Errors.MustSpecifyExtensionID;
134
+ }
135
+
136
+ const tooManyArguments = args.length > 2;
137
+ const incorrectConnectInfoType = connectInfo && typeof connectInfo !== 'object';
138
+ if (tooManyArguments || incorrectConnectInfoType) {
139
+ throw Errors.NoMatchingSignature;
140
+ }
141
+
142
+ const extensionIdIsString = typeof extensionId === 'string';
143
+ if (extensionIdIsString && extensionId === '') {
144
+ throw Errors.MustSpecifyExtensionID;
145
+ }
146
+ if (extensionIdIsString && !isValidExtensionID(extensionId)) {
147
+ throw Errors.InvalidExtensionID;
148
+ }
149
+
150
+ if (typeof extensionId === 'object') {
151
+ if (args.length > 1) throw Errors.NoMatchingSignature;
152
+ if (Object.keys(extensionId).length === 0) throw Errors.MustSpecifyExtensionID;
153
+ throw Errors.MustSpecifyExtensionID;
154
+ }
155
+
156
+ return utils.patchToStringNested(makeConnectResponse());
157
+ }
158
+ };
159
+
160
+ utils.mockWithProxy(
161
+ window.chrome.runtime,
162
+ 'connect',
163
+ function connect() {},
164
+ connectHandler
165
+ );
166
+
167
+ function makeConnectResponse() {
168
+ const onSomething = () => ({
169
+ addListener: function addListener() {},
170
+ dispatch: function dispatch() {},
171
+ hasListener: function hasListener() {},
172
+ hasListeners: function hasListeners() { return false; },
173
+ removeListener: function removeListener() {}
174
+ });
175
+
176
+ return {
177
+ name: '',
178
+ sender: undefined,
179
+ disconnect: function disconnect() {},
180
+ onDisconnect: onSomething(),
181
+ onMessage: onSomething(),
182
+ postMessage: function postMessage() {
183
+ if (!arguments.length) {
184
+ throw new TypeError('Insufficient number of arguments.');
185
+ }
186
+ throw new Error('Attempting to use a disconnected port object');
187
+ }
188
+ };
189
+ }
190
+ })();
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Evasion: iframe.contentWindow
3
+ * Fix for the HEADCHR_IFRAME detection (iframe.contentWindow.chrome).
4
+ * Only srcdoc powered iframes cause issues due to a chromium bug.
5
+ */
6
+ (function() {
7
+ 'use strict';
8
+
9
+ const utils = window._stealthUtils;
10
+ if (!utils) return;
11
+
12
+ try {
13
+ // Adds a contentWindow proxy to the provided iframe element
14
+ const addContentWindowProxy = iframe => {
15
+ const contentWindowProxy = {
16
+ get(target, key) {
17
+ // Make this thing behave like a regular iframe window
18
+ if (key === 'self') {
19
+ return this;
20
+ }
21
+ if (key === 'frameElement') {
22
+ return iframe;
23
+ }
24
+ // Intercept iframe.contentWindow[0] to hide the property 0 added by the proxy
25
+ if (key === '0') {
26
+ return undefined;
27
+ }
28
+ return Reflect.get(target, key);
29
+ }
30
+ };
31
+
32
+ if (!iframe.contentWindow) {
33
+ const proxy = new Proxy(window, contentWindowProxy);
34
+ Object.defineProperty(iframe, 'contentWindow', {
35
+ get() {
36
+ return proxy;
37
+ },
38
+ set(newValue) {
39
+ return newValue; // contentWindow is immutable
40
+ },
41
+ enumerable: true,
42
+ configurable: false
43
+ });
44
+ }
45
+ };
46
+
47
+ // Handles iframe element creation, augments srcdoc property
48
+ const handleIframeCreation = (target, thisArg, args) => {
49
+ const iframe = target.apply(thisArg, args);
50
+
51
+ const _iframe = iframe;
52
+ const _srcdoc = _iframe.srcdoc;
53
+
54
+ // Add hook for the srcdoc property
55
+ Object.defineProperty(iframe, 'srcdoc', {
56
+ configurable: true,
57
+ get: function() {
58
+ return _srcdoc;
59
+ },
60
+ set: function(newValue) {
61
+ addContentWindowProxy(this);
62
+ // Reset property, the hook is only needed once
63
+ Object.defineProperty(iframe, 'srcdoc', {
64
+ configurable: false,
65
+ writable: false,
66
+ value: _srcdoc
67
+ });
68
+ _iframe.srcdoc = newValue;
69
+ }
70
+ });
71
+ return iframe;
72
+ };
73
+
74
+ // Adds a hook to intercept iframe creation events
75
+ const addIframeCreationSniffer = () => {
76
+ const createElementHandler = {
77
+ get(target, key) {
78
+ return Reflect.get(target, key);
79
+ },
80
+ apply: function(target, thisArg, args) {
81
+ const isIframe =
82
+ args && args.length && `${args[0]}`.toLowerCase() === 'iframe';
83
+ if (!isIframe) {
84
+ return target.apply(thisArg, args);
85
+ } else {
86
+ return handleIframeCreation(target, thisArg, args);
87
+ }
88
+ }
89
+ };
90
+ utils.replaceWithProxy(
91
+ document,
92
+ 'createElement',
93
+ createElementHandler
94
+ );
95
+ };
96
+
97
+ addIframeCreationSniffer();
98
+ } catch (err) {
99
+ // Silently fail
100
+ }
101
+ })();
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Evasion: media.codecs
3
+ * Fix Chromium not reporting "probably" to proprietary codecs.
4
+ * Chromium doesn't support proprietary codecs, only Chrome does.
5
+ */
6
+ (function() {
7
+ 'use strict';
8
+
9
+ const utils = window._stealthUtils;
10
+ if (!utils) return;
11
+
12
+ /**
13
+ * Parse media type input to extract mime type and codecs.
14
+ * @param {String} arg - Input like 'video/mp4; codecs="avc1.42E01E"'
15
+ */
16
+ const parseInput = arg => {
17
+ const [mime, codecStr] = arg.trim().split(';');
18
+ let codecs = [];
19
+ if (codecStr && codecStr.includes('codecs="')) {
20
+ codecs = codecStr
21
+ .trim()
22
+ .replace(`codecs="`, '')
23
+ .replace(`"`, '')
24
+ .trim()
25
+ .split(',')
26
+ .filter(x => !!x)
27
+ .map(x => x.trim());
28
+ }
29
+ return { mime, codecStr, codecs };
30
+ };
31
+
32
+ const canPlayType = {
33
+ apply: function(target, ctx, args) {
34
+ if (!args || !args.length) {
35
+ return target.apply(ctx, args);
36
+ }
37
+ const { mime, codecs } = parseInput(args[0]);
38
+
39
+ // This specific mp4 codec is missing in Chromium
40
+ if (mime === 'video/mp4') {
41
+ if (codecs.includes('avc1.42E01E')) {
42
+ return 'probably';
43
+ }
44
+ }
45
+ // This mimetype is only supported if no codecs are specified
46
+ if (mime === 'audio/x-m4a' && !codecs.length) {
47
+ return 'maybe';
48
+ }
49
+ // This mimetype is only supported if no codecs are specified
50
+ if (mime === 'audio/aac' && !codecs.length) {
51
+ return 'probably';
52
+ }
53
+ // Everything else as usual
54
+ return target.apply(ctx, args);
55
+ }
56
+ };
57
+
58
+ if (typeof HTMLMediaElement !== 'undefined') {
59
+ utils.replaceWithProxy(
60
+ HTMLMediaElement.prototype,
61
+ 'canPlayType',
62
+ canPlayType
63
+ );
64
+ }
65
+ })();
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Evasion: navigator.hardwareConcurrency
3
+ * Set the hardwareConcurrency to a reasonable value (default: 4).
4
+ */
5
+ (function(opts) {
6
+ 'use strict';
7
+
8
+ const utils = window._stealthUtils;
9
+ if (!utils) return;
10
+
11
+ const hardwareConcurrency = opts.hardwareConcurrency || 4;
12
+
13
+ utils.replaceGetterWithProxy(
14
+ Object.getPrototypeOf(navigator),
15
+ 'hardwareConcurrency',
16
+ utils.makeHandler().getterValue(hardwareConcurrency)
17
+ );
18
+ })({ hardwareConcurrency: null }); // Will be replaced by Ruby
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Evasion: navigator.languages
3
+ * Override navigator.languages to match Accept-Language header.
4
+ */
5
+ (function(opts) {
6
+ 'use strict';
7
+
8
+ const utils = window._stealthUtils;
9
+ if (!utils) return;
10
+
11
+ const languages = opts.languages || ['en-US', 'en'];
12
+
13
+ utils.replaceGetterWithProxy(
14
+ Object.getPrototypeOf(navigator),
15
+ 'languages',
16
+ utils.makeHandler().getterValue(Object.freeze([...languages]))
17
+ );
18
+ })({ languages: null }); // Will be replaced by Ruby
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Evasion: navigator.permissions
3
+ * Fix Notification.permission behaving weirdly in headless mode.
4
+ * On secure origins the permission should be "default", not "denied".
5
+ */
6
+ (function() {
7
+ 'use strict';
8
+
9
+ const utils = window._stealthUtils;
10
+ if (!utils) return;
11
+
12
+ const isSecure = document.location.protocol.startsWith('https');
13
+
14
+ // In headful on secure origins the permission should be "default", not "denied"
15
+ if (isSecure) {
16
+ if (typeof Notification !== 'undefined') {
17
+ utils.replaceGetterWithProxy(Notification, 'permission', {
18
+ apply() {
19
+ return 'default';
20
+ }
21
+ });
22
+ }
23
+ }
24
+
25
+ // On insecure origins in headful the state is "denied",
26
+ // whereas in headless it's "prompt"
27
+ if (!isSecure) {
28
+ if (typeof Permissions !== 'undefined' && typeof PermissionStatus !== 'undefined') {
29
+ const handler = {
30
+ apply(target, ctx, args) {
31
+ const param = (args || [])[0];
32
+
33
+ const isNotifications =
34
+ param && param.name && param.name === 'notifications';
35
+ if (!isNotifications) {
36
+ return utils.cache.Reflect.apply(...arguments);
37
+ }
38
+
39
+ return Promise.resolve(
40
+ Object.setPrototypeOf(
41
+ {
42
+ state: 'denied',
43
+ onchange: null
44
+ },
45
+ PermissionStatus.prototype
46
+ )
47
+ );
48
+ }
49
+ };
50
+ utils.replaceWithProxy(Permissions.prototype, 'query', handler);
51
+ }
52
+ }
53
+ })();
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Evasion: navigator.plugins
3
+ * In headless mode navigator.mimeTypes and navigator.plugins are empty.
4
+ * This plugin emulates both with functional mocks to match regular headful Chrome.
5
+ */
6
+ (function() {
7
+ 'use strict';
8
+
9
+ const utils = window._stealthUtils;
10
+ if (!utils) return;
11
+
12
+ // That means we're running headful
13
+ const hasPlugins = 'plugins' in navigator && navigator.plugins.length;
14
+ if (hasPlugins) {
15
+ return; // Nothing to do here
16
+ }
17
+
18
+ // Plugin and MimeType data (matches Chrome)
19
+ const mimeTypesData = [
20
+ {
21
+ type: 'application/pdf',
22
+ suffixes: 'pdf',
23
+ description: '',
24
+ __pluginName: 'Chrome PDF Viewer'
25
+ },
26
+ {
27
+ type: 'application/x-google-chrome-pdf',
28
+ suffixes: 'pdf',
29
+ description: 'Portable Document Format',
30
+ __pluginName: 'Chrome PDF Plugin'
31
+ },
32
+ {
33
+ type: 'application/x-nacl',
34
+ suffixes: '',
35
+ description: 'Native Client Executable',
36
+ __pluginName: 'Native Client'
37
+ },
38
+ {
39
+ type: 'application/x-pnacl',
40
+ suffixes: '',
41
+ description: 'Portable Native Client Executable',
42
+ __pluginName: 'Native Client'
43
+ }
44
+ ];
45
+
46
+ const pluginsData = [
47
+ {
48
+ name: 'Chrome PDF Plugin',
49
+ filename: 'internal-pdf-viewer',
50
+ description: 'Portable Document Format',
51
+ __mimeTypes: ['application/x-google-chrome-pdf']
52
+ },
53
+ {
54
+ name: 'Chrome PDF Viewer',
55
+ filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai',
56
+ description: '',
57
+ __mimeTypes: ['application/pdf']
58
+ },
59
+ {
60
+ name: 'Native Client',
61
+ filename: 'internal-nacl-plugin',
62
+ description: '',
63
+ __mimeTypes: ['application/x-nacl', 'application/x-pnacl']
64
+ }
65
+ ];
66
+
67
+ // Helper to define properties with vanilla descriptors
68
+ const defineProp = (obj, prop, value) =>
69
+ Object.defineProperty(obj, prop, {
70
+ value,
71
+ writable: false,
72
+ enumerable: false,
73
+ configurable: true
74
+ });
75
+
76
+ // Generate function mocks for item/namedItem/refresh
77
+ const generateFunctionMocks = (proto, itemMainProp, dataArray) => ({
78
+ item: utils.createProxy(proto.item, {
79
+ apply(target, ctx, args) {
80
+ if (!args.length) {
81
+ throw new TypeError(
82
+ `Failed to execute 'item' on '${proto[Symbol.toStringTag]}': 1 argument required, but only 0 present.`
83
+ );
84
+ }
85
+ const isInteger = args[0] && Number.isInteger(Number(args[0]));
86
+ return (isInteger ? dataArray[Number(args[0])] : dataArray[0]) || null;
87
+ }
88
+ }),
89
+ namedItem: utils.createProxy(proto.namedItem, {
90
+ apply(target, ctx, args) {
91
+ if (!args.length) {
92
+ throw new TypeError(
93
+ `Failed to execute 'namedItem' on '${proto[Symbol.toStringTag]}': 1 argument required, but only 0 present.`
94
+ );
95
+ }
96
+ return dataArray.find(mt => mt[itemMainProp] === args[0]) || null;
97
+ }
98
+ }),
99
+ refresh: proto.refresh
100
+ ? utils.createProxy(proto.refresh, {
101
+ apply(target, ctx, args) {
102
+ return undefined;
103
+ }
104
+ })
105
+ : undefined
106
+ });
107
+
108
+ // Generate a magic array (MimeTypeArray or PluginArray)
109
+ const generateMagicArray = (
110
+ dataArray,
111
+ proto,
112
+ itemProto,
113
+ itemMainProp
114
+ ) => {
115
+ const makeItem = data => {
116
+ const item = {};
117
+ for (const prop of Object.keys(data)) {
118
+ if (prop.startsWith('__')) {
119
+ continue;
120
+ }
121
+ defineProp(item, prop, data[prop]);
122
+ }
123
+ return patchItem(item, data);
124
+ };
125
+
126
+ const patchItem = (item, data) => {
127
+ let descriptor = Object.getOwnPropertyDescriptors(item);
128
+
129
+ // Plugins have a magic length property
130
+ if (itemProto === Plugin.prototype) {
131
+ descriptor = {
132
+ ...descriptor,
133
+ length: {
134
+ value: data.__mimeTypes.length,
135
+ writable: false,
136
+ enumerable: false,
137
+ configurable: true
138
+ }
139
+ };
140
+ }
141
+
142
+ const obj = Object.create(itemProto, descriptor);
143
+ const blacklist = [...Object.keys(data), 'length', 'enabledPlugin'];
144
+
145
+ return new Proxy(obj, {
146
+ ownKeys(target) {
147
+ return Reflect.ownKeys(target).filter(k => !blacklist.includes(k));
148
+ },
149
+ getOwnPropertyDescriptor(target, prop) {
150
+ if (blacklist.includes(prop)) {
151
+ return undefined;
152
+ }
153
+ return Reflect.getOwnPropertyDescriptor(target, prop);
154
+ }
155
+ });
156
+ };
157
+
158
+ const magicArray = [];
159
+
160
+ dataArray.forEach(data => {
161
+ magicArray.push(makeItem(data));
162
+ });
163
+
164
+ // Add direct property access based on types
165
+ magicArray.forEach(entry => {
166
+ defineProp(magicArray, entry[itemMainProp], entry);
167
+ });
168
+
169
+ const magicArrayObj = Object.create(proto, {
170
+ ...Object.getOwnPropertyDescriptors(magicArray),
171
+ length: {
172
+ value: magicArray.length,
173
+ writable: false,
174
+ enumerable: false,
175
+ configurable: true
176
+ }
177
+ });
178
+
179
+ const functionMocks = generateFunctionMocks(proto, itemMainProp, magicArray);
180
+
181
+ const magicArrayObjProxy = new Proxy(magicArrayObj, {
182
+ get(target, key = '') {
183
+ if (key === 'item') {
184
+ return functionMocks.item;
185
+ }
186
+ if (key === 'namedItem') {
187
+ return functionMocks.namedItem;
188
+ }
189
+ if (proto === PluginArray.prototype && key === 'refresh') {
190
+ return functionMocks.refresh;
191
+ }
192
+ return utils.cache.Reflect.get(...arguments);
193
+ },
194
+ ownKeys(target) {
195
+ const keys = [];
196
+ const typeProps = magicArray.map(mt => mt[itemMainProp]);
197
+ typeProps.forEach((_, i) => keys.push(`${i}`));
198
+ typeProps.forEach(propName => keys.push(propName));
199
+ return keys;
200
+ },
201
+ getOwnPropertyDescriptor(target, prop) {
202
+ if (prop === 'length') {
203
+ return undefined;
204
+ }
205
+ return Reflect.getOwnPropertyDescriptor(target, prop);
206
+ }
207
+ });
208
+
209
+ return magicArrayObjProxy;
210
+ };
211
+
212
+ // Generate MimeTypeArray
213
+ const mimeTypes = generateMagicArray(
214
+ mimeTypesData,
215
+ MimeTypeArray.prototype,
216
+ MimeType.prototype,
217
+ 'type'
218
+ );
219
+
220
+ // Generate PluginArray
221
+ const plugins = generateMagicArray(
222
+ pluginsData,
223
+ PluginArray.prototype,
224
+ Plugin.prototype,
225
+ 'name'
226
+ );
227
+
228
+ // Cross-reference plugins and mimeTypes
229
+ for (const pluginData of pluginsData) {
230
+ pluginData.__mimeTypes.forEach((type, index) => {
231
+ plugins[pluginData.name][index] = mimeTypes[type];
232
+
233
+ Object.defineProperty(plugins[pluginData.name], type, {
234
+ value: mimeTypes[type],
235
+ writable: false,
236
+ enumerable: false,
237
+ configurable: true
238
+ });
239
+ Object.defineProperty(mimeTypes[type], 'enabledPlugin', {
240
+ value:
241
+ type === 'application/x-pnacl'
242
+ ? mimeTypes['application/x-nacl'].enabledPlugin
243
+ : new Proxy(plugins[pluginData.name], {}),
244
+ writable: false,
245
+ enumerable: false,
246
+ configurable: true
247
+ });
248
+ });
249
+ }
250
+
251
+ // Patch navigator
252
+ const patchNavigator = (name, value) =>
253
+ utils.replaceProperty(Object.getPrototypeOf(navigator), name, {
254
+ get() {
255
+ return value;
256
+ }
257
+ });
258
+
259
+ patchNavigator('mimeTypes', mimeTypes);
260
+ patchNavigator('plugins', plugins);
261
+ })();