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