Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
QuiteAFancyEmerald
GitHub Repository: QuiteAFancyEmerald/Holy-Unblocker
Path: blob/master/lib/rammerhead/src/client/rammerhead.js
6530 views
1
(function () {
2
var hammerhead = window['%hammerhead%'];
3
if (!hammerhead) throw new Error('hammerhead not loaded yet');
4
if (hammerhead.settings._settings.sessionId) {
5
// task.js already loaded. this will likely never happen though since this file loads before task.js
6
console.warn('unexpected task.js to load before rammerhead.js. url shuffling cannot be used');
7
main();
8
} else {
9
// wait for task.js to load
10
hookHammerheadStartOnce(main);
11
// before task.js, we need to add url shuffling
12
addUrlShuffling();
13
}
14
15
function main() {
16
fixUrlRewrite();
17
fixElementGetter();
18
fixCrossWindowLocalStorage();
19
20
delete window.overrideGetProxyUrl;
21
delete window.overrideParseProxyUrl;
22
delete window.overrideIsCrossDomainWindows;
23
24
// other code if they want to also hook onto hammerhead start //
25
if (window.rammerheadStartListeners) {
26
for (const eachListener of window.rammerheadStartListeners) {
27
try {
28
eachListener();
29
} catch (e) {
30
console.error(e);
31
}
32
}
33
delete window.rammerheadStartListeners;
34
}
35
36
// sync localStorage code //
37
// disable if other code wants to implement their own localStorage site wrapper
38
if (window.rammerheadDisableLocalStorageImplementation) {
39
delete window.rammerheadDisableLocalStorageImplementation;
40
return;
41
}
42
// consts
43
var timestampKey = 'rammerhead_synctimestamp';
44
var updateInterval = 5000;
45
var isSyncing = false;
46
47
var proxiedLocalStorage = localStorage;
48
var realLocalStorage = proxiedLocalStorage.internal.nativeStorage;
49
var sessionId = hammerhead.settings._settings.sessionId;
50
var origin = window.__get$(window, 'location').origin;
51
var keyChanges = [];
52
53
try {
54
syncLocalStorage();
55
} catch (e) {
56
if (e.message !== 'server wants to disable localStorage syncing') {
57
throw e;
58
}
59
return;
60
}
61
proxiedLocalStorage.addChangeEventListener(function (event) {
62
if (isSyncing) return;
63
if (keyChanges.indexOf(event.key) === -1) keyChanges.push(event.key);
64
});
65
setInterval(function () {
66
var update = compileUpdate();
67
if (!update) return;
68
localStorageRequest({ type: 'update', updateData: update }, function (data) {
69
updateTimestamp(data.timestamp);
70
});
71
72
keyChanges = [];
73
}, updateInterval);
74
document.addEventListener('visibilitychange', function () {
75
if (document.visibilityState === 'hidden') {
76
var update = compileUpdate();
77
if (update) {
78
// even though we'll never get the timestamp, it's fine. this way,
79
// the data is safer
80
hammerhead.nativeMethods.sendBeacon.call(
81
window.navigator,
82
getSyncStorageEndpoint(),
83
JSON.stringify({
84
type: 'update',
85
updateData: update
86
})
87
);
88
}
89
}
90
});
91
92
function syncLocalStorage() {
93
isSyncing = true;
94
var timestamp = getTimestamp();
95
var response;
96
if (!timestamp) {
97
// first time syncing
98
response = localStorageRequest({ type: 'sync', fetch: true });
99
if (response.timestamp) {
100
updateTimestamp(response.timestamp);
101
overwriteLocalStorage(response.data);
102
}
103
} else {
104
// resync
105
response = localStorageRequest({ type: 'sync', timestamp: timestamp, data: proxiedLocalStorage });
106
if (response.timestamp) {
107
updateTimestamp(response.timestamp);
108
overwriteLocalStorage(response.data);
109
}
110
}
111
isSyncing = false;
112
113
function overwriteLocalStorage(data) {
114
if (!data || typeof data !== 'object') throw new TypeError('data must be an object');
115
proxiedLocalStorage.clear();
116
for (var prop in data) {
117
proxiedLocalStorage[prop] = data[prop];
118
}
119
}
120
}
121
function updateTimestamp(timestamp) {
122
if (!timestamp) throw new TypeError('timestamp must be defined');
123
if (isNaN(parseInt(timestamp))) throw new TypeError('timestamp must be a number. received' + timestamp);
124
realLocalStorage[timestampKey] = timestamp;
125
}
126
function getTimestamp() {
127
var rawTimestamp = realLocalStorage[timestampKey];
128
var timestamp = parseInt(rawTimestamp);
129
if (isNaN(timestamp)) {
130
if (rawTimestamp) {
131
console.warn('invalid timestamp retrieved from storage: ' + rawTimestamp);
132
}
133
return null;
134
}
135
return timestamp;
136
}
137
function getSyncStorageEndpoint() {
138
return (
139
'/syncLocalStorage?sessionId=' + encodeURIComponent(sessionId) + '&origin=' + encodeURIComponent(origin)
140
);
141
}
142
function localStorageRequest(data, callback) {
143
if (!data || typeof data !== 'object') throw new TypeError('data must be an object');
144
145
var request = hammerhead.createNativeXHR();
146
// make synchronous if there is no callback
147
request.open('POST', getSyncStorageEndpoint(), !!callback);
148
request.setRequestHeader('content-type', 'application/json');
149
request.send(JSON.stringify(data));
150
function check() {
151
if (request.status === 404) {
152
throw new Error('server wants to disable localStorage syncing');
153
}
154
if (request.status !== 200)
155
throw new Error(
156
'server sent a non 200 code. got ' + request.status + '. Response: ' + request.responseText
157
);
158
}
159
if (!callback) {
160
check();
161
return JSON.parse(request.responseText);
162
} else {
163
request.onload = function () {
164
check();
165
callback(JSON.parse(request.responseText));
166
};
167
}
168
}
169
function compileUpdate() {
170
if (!keyChanges.length) return null;
171
172
var updates = {};
173
for (var i = 0; i < keyChanges.length; i++) {
174
updates[keyChanges[i]] = proxiedLocalStorage[keyChanges[i]];
175
}
176
177
keyChanges = [];
178
return updates;
179
}
180
}
181
182
var noShuffling = false;
183
function addUrlShuffling() {
184
const request = new XMLHttpRequest();
185
const sessionId = (location.pathname.slice(1).match(/^[a-z0-9]+/i) || [])[0];
186
if (!sessionId) {
187
console.warn('cannot get session id from url');
188
return;
189
}
190
request.open('GET', '/rammer/api/shuffleDict?id=' + sessionId, false);
191
request.send();
192
if (request.status !== 200) {
193
console.warn(
194
`received a non 200 status code while trying to fetch shuffleDict:\nstatus: ${request.status}\nresponse: ${request.responseText}`
195
);
196
return;
197
}
198
const shuffleDict = JSON.parse(request.responseText);
199
if (!shuffleDict) return;
200
201
// pasting entire thing here "because lazy" - m28
202
const mod = (n, m) => ((n % m) + m) % m;
203
const baseDictionary = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~-';
204
const shuffledIndicator = '_rhs';
205
const generateDictionary = function () {
206
let str = '';
207
const split = baseDictionary.split('');
208
while (split.length > 0) {
209
str += split.splice(Math.floor(Math.random() * split.length), 1)[0];
210
}
211
return str;
212
};
213
class StrShuffler {
214
constructor(dictionary = generateDictionary()) {
215
this.dictionary = dictionary;
216
}
217
shuffle(str) {
218
if (str.startsWith(shuffledIndicator)) {
219
return str;
220
}
221
let shuffledStr = '';
222
for (let i = 0; i < str.length; i++) {
223
const char = str.charAt(i);
224
const idx = baseDictionary.indexOf(char);
225
if (char === '%' && str.length - i >= 3) {
226
shuffledStr += char;
227
shuffledStr += str.charAt(++i);
228
shuffledStr += str.charAt(++i);
229
} else if (idx === -1) {
230
shuffledStr += char;
231
} else {
232
shuffledStr += this.dictionary.charAt(mod(idx + i, baseDictionary.length));
233
}
234
}
235
return shuffledIndicator + shuffledStr;
236
}
237
unshuffle(str) {
238
if (!str.startsWith(shuffledIndicator)) {
239
return str;
240
}
241
242
str = str.slice(shuffledIndicator.length);
243
244
let unshuffledStr = '';
245
for (let i = 0; i < str.length; i++) {
246
const char = str.charAt(i);
247
const idx = this.dictionary.indexOf(char);
248
if (char === '%' && str.length - i >= 3) {
249
unshuffledStr += char;
250
unshuffledStr += str.charAt(++i);
251
unshuffledStr += str.charAt(++i);
252
} else if (idx === -1) {
253
unshuffledStr += char;
254
} else {
255
unshuffledStr += baseDictionary.charAt(mod(idx - i, baseDictionary.length));
256
}
257
}
258
return unshuffledStr;
259
}
260
}
261
262
function patch(url) {
263
// url = _rhsEPrcb://bqhQko.tHR/
264
// remove slash
265
return url.replace(/(^.*?:\/)\//, '$1');
266
}
267
268
function unpatch(url) {
269
// url = _rhsEPrcb:/bqhQko.tHR/
270
// restore slash
271
return url.replace(/^.*?:\/(?!\/)/, '$&/');
272
}
273
274
const replaceUrl = (url, replacer) => {
275
// regex: https://google.com/ sessionid/ url
276
return (url || '').replace(/^((?:[a-z0-9]+:\/\/[^/]+)?(?:\/[^/]+\/))([^]+)/i, function (_, g1, g2) {
277
return g1 + replacer(g2);
278
})
279
};
280
281
const shuffler = new StrShuffler(shuffleDict);
282
283
// shuffle current url if it isn't already shuffled (unshuffled urls likely come from user input)
284
const oldUrl = location.href;
285
const newUrl = replaceUrl(location.href, (url) => shuffler.shuffle(url));
286
if (oldUrl !== newUrl) {
287
history.replaceState(null, null, newUrl);
288
}
289
290
const getProxyUrl = hammerhead.utils.url.getProxyUrl;
291
const parseProxyUrl = hammerhead.utils.url.parseProxyUrl;
292
hammerhead.utils.url.overrideGetProxyUrl(function (url, opts) {
293
if (noShuffling) {
294
return getProxyUrl(url, opts);
295
}
296
return replaceUrl(getProxyUrl(url, opts), (u) => patch(shuffler.shuffle(u)), true)
297
});
298
hammerhead.utils.url.overrideParseProxyUrl(function (url) {
299
return parseProxyUrl(replaceUrl(url, (u) => shuffler.unshuffle(unpatch(u)), false));
300
});
301
// manual hooks //
302
window.overrideGetProxyUrl(
303
(getProxyUrl$1) =>
304
function (url, opts) {
305
if (noShuffling) {
306
return getProxyUrl$1(url, opts);
307
}
308
return replaceUrl(getProxyUrl$1(url, opts), (u) => patch(shuffler.shuffle(u)), true);
309
}
310
);
311
window.overrideParseProxyUrl(
312
(parseProxyUrl$1) =>
313
function (url) {
314
return parseProxyUrl$1(replaceUrl(url, (u) => shuffler.unshuffle(unpatch(u)), false));
315
}
316
);
317
}
318
function fixUrlRewrite() {
319
const port = location.port || (location.protocol === 'https:' ? '443' : '80');
320
const getProxyUrl = hammerhead.utils.url.getProxyUrl;
321
hammerhead.utils.url.overrideGetProxyUrl(function (url, opts = {}) {
322
if (!opts.proxyPort) {
323
opts.proxyPort = port;
324
}
325
return getProxyUrl(url, opts);
326
});
327
window.overrideParseProxyUrl(
328
(parseProxyUrl$1) =>
329
function (url) {
330
const parsed = parseProxyUrl$1(url);
331
if (!parsed || !parsed.proxy) return parsed;
332
if (!parsed.proxy.port) {
333
parsed.proxy.port = port;
334
}
335
return parsed;
336
}
337
);
338
}
339
function fixElementGetter() {
340
const fixList = {
341
HTMLAnchorElement: ['href'],
342
HTMLAreaElement: ['href'],
343
HTMLBaseElement: ['href'],
344
HTMLEmbedElement: ['src'],
345
HTMLFormElement: ['action'],
346
HTMLFrameElement: ['src'],
347
HTMLIFrameElement: ['src'],
348
HTMLImageElement: ['src'],
349
HTMLInputElement: ['src'],
350
HTMLLinkElement: ['href'],
351
HTMLMediaElement: ['src'],
352
HTMLModElement: ['cite'],
353
HTMLObjectElement: ['data'],
354
HTMLQuoteElement: ['cite'],
355
HTMLScriptElement: ['src'],
356
HTMLSourceElement: ['src'],
357
HTMLTrackElement: ['src']
358
};
359
const urlRewrite = (url) => (hammerhead.utils.url.parseProxyUrl(url) || {}).destUrl || url;
360
for (const ElementClass in fixList) {
361
for (const attr of fixList[ElementClass]) {
362
if (!window[ElementClass]) {
363
console.warn('unexpected unsupported element class ' + ElementClass);
364
continue;
365
}
366
const desc = Object.getOwnPropertyDescriptor(window[ElementClass].prototype, attr);
367
const originalGet = desc.get;
368
desc.get = function () {
369
return urlRewrite(originalGet.call(this));
370
};
371
if (attr === 'action') {
372
const originalSet = desc.set;
373
// don't shuffle form action urls
374
desc.set = function (value) {
375
noShuffling = true;
376
try {
377
var returnVal = originalSet.call(this, value);
378
} catch (e) {
379
noShuffling = false;
380
throw e;
381
}
382
noShuffling = false;
383
return returnVal;
384
};
385
}
386
Object.defineProperty(window[ElementClass].prototype, attr, desc);
387
}
388
}
389
}
390
function fixCrossWindowLocalStorage() {
391
// completely replace hammerhead's implementation as restore() and save() on every
392
// call is just not viable (mainly memory issues as the garbage collector is sometimes not fast enough)
393
394
const prefix = `rammerhead|storage-wrapper|${hammerhead.settings._settings.sessionId}|${
395
window.__get$(window, 'location').host
396
}|`;
397
const toRealStorageKey = (key = '') => prefix + key;
398
const fromRealStorageKey = (key = '') => {
399
if (!key.startsWith(prefix)) return null;
400
return key.slice(prefix.length);
401
};
402
403
const replaceStorageInstance = (storageProp, realStorage) => {
404
const reservedProps = ['internal', 'clear', 'key', 'getItem', 'setItem', 'removeItem', 'length'];
405
Object.defineProperty(window, storageProp, {
406
// define a value-based instead of getter-based property, since with this localStorage implementation,
407
// we don't need to rely on sharing a single memory-based storage across frames, unlike hammerhead
408
configurable: true,
409
writable: true,
410
// still use window[storageProp] as basis to allow scripts to access localStorage.internal
411
value: new Proxy(window[storageProp], {
412
get(target, prop, receiver) {
413
if (reservedProps.includes(prop) && prop !== 'length') {
414
return Reflect.get(target, prop, receiver);
415
} else if (prop === 'length') {
416
let len = 0;
417
for (const [key] of Object.entries(realStorage)) {
418
if (fromRealStorageKey(key)) len++;
419
}
420
return len;
421
} else {
422
return realStorage[toRealStorageKey(prop)];
423
}
424
},
425
set(_, prop, value) {
426
if (!reservedProps.includes(prop)) {
427
realStorage[toRealStorageKey(prop)] = value;
428
}
429
return true;
430
},
431
deleteProperty(_, prop) {
432
delete realStorage[toRealStorageKey(prop)];
433
return true;
434
},
435
has(target, prop) {
436
return toRealStorageKey(prop) in realStorage || prop in target;
437
},
438
ownKeys() {
439
const list = [];
440
for (const [key] of Object.entries(realStorage)) {
441
const proxyKey = fromRealStorageKey(key);
442
if (proxyKey && !reservedProps.includes(proxyKey)) list.push(proxyKey);
443
}
444
return list;
445
},
446
getOwnPropertyDescriptor(_, prop) {
447
return Object.getOwnPropertyDescriptor(realStorage, toRealStorageKey(prop));
448
},
449
defineProperty(_, prop, desc) {
450
if (!reservedProps.includes(prop)) {
451
Object.defineProperty(realStorage, toRealStorageKey(prop), desc);
452
}
453
return true;
454
}
455
})
456
});
457
};
458
const rewriteFunction = (prop, newFunc) => {
459
Storage.prototype[prop] = new Proxy(Storage.prototype[prop], {
460
apply(_, thisArg, args) {
461
return newFunc.apply(thisArg, args);
462
}
463
});
464
};
465
466
replaceStorageInstance('localStorage', hammerhead.storages.localStorageProxy.internal.nativeStorage);
467
replaceStorageInstance('sessionStorage', hammerhead.storages.sessionStorageProxy.internal.nativeStorage);
468
rewriteFunction('clear', function () {
469
for (const [key] of Object.entries(this)) {
470
delete this[key];
471
}
472
});
473
rewriteFunction('key', function (keyNum) {
474
return (Object.entries(this)[keyNum] || [])[0] || null;
475
});
476
rewriteFunction('getItem', function (key) {
477
return this.internal.nativeStorage[toRealStorageKey(key)] || null;
478
});
479
rewriteFunction('setItem', function (key, value) {
480
if (key) {
481
this.internal.nativeStorage[toRealStorageKey(key)] = value;
482
}
483
});
484
rewriteFunction('removeItem', function (key) {
485
delete this.internal.nativeStorage[toRealStorageKey(key)];
486
});
487
}
488
489
function hookHammerheadStartOnce(callback) {
490
var originalStart = hammerhead.__proto__.start;
491
hammerhead.__proto__.start = function () {
492
originalStart.apply(this, arguments);
493
hammerhead.__proto__.start = originalStart;
494
callback();
495
};
496
}
497
})();
498
499