Path: blob/master/src/source-rewrites.mjs
5156 views
import { existsSync, readFileSync } from 'node:fs';1import {2config,3serverUrl,4flatAltPaths,5cookingInserts,6vegetables,7charRandom,8delimiter,9textMasks,10splashRandom,11cacheBustList,12versionValue,13uvError,14sjError,15} from './routes.mjs';16export { paintSource as default };1718/* Below are lots of function definitions used to obfuscate the website.19* This makes the website harder to properly categorize, as its source code20* changes with each time it is compiled using npm run build.21*22* For customizing source code transformation and more, see the config.json file.23* For automatically recompiling in production mode, see ecosystem.config.js.24*/25const regExpEscape = /[-[\]{}()*+?.,\\^$#\s]/g,26basicStrEscape = /["'`$\\]/g,27charset = /­|​|­|<wbr>/gi,28subtermsByCaps = /[A-Z]?[^A-Z]+|[A-Z]/g,29subtermsByVowels = /(?<=[AEIOUYaeiouy])(?!$)/g,30termsBySpaces = /\S+/g,31containsMask = /&#\d+;|&#x[A-z\d]+;|&[A-z]+;/,32getEndPoint = /((?<![^\/])github\/)?[^\/]+$/,33getPaths = /[^\/]+(?=\/)/g,34getAbsoluteRoot = /^~?\/+|^~$|^(?!\.\/)/,35getRoutePath = /(?<={{route}}{{\s*)[^}\s]+(?=\s*}})/,36getAttrPath = /(?<=(?:src|href)=(["']?))[\w\.~:\/\?#[\]@!$&()*+,;%=-]+(?=\1)/,37getAttrValues = /=(['"])(?:(?!\1)[^])+\1/g,38getNodesByLine = /(?<=^\s*)\S.*?(?=\s*$)/gm,39routeConditions = {40inline: config.disguiseFiles && config.minifyScripts,41},42applyInsert = (str, insertFunction, numArgs = 0) => {43const mode = 'function' === typeof insertFunction,44keyword = mode ? insertFunction.name : insertFunction,45replaceParams1 = new RegExp(46`[^\\S\\n\\r]*{{${keyword}}}\\s*` +47'\\s*{{\\s*\\n\\r?((?:(?!}})[^])*\\n\\r?)\\s*}}\\s*?\\n?\\r?'.repeat(48numArgs49),50'g'51),52replaceParams2 = new RegExp(53`{{${keyword}}}` + '{{((?:(?!}})[^])*?)}}'.repeat(numArgs),54'g'55),56replaceFunc = mode57? (text, ...captures) => insertFunction(...captures.splice(0, numArgs))58: (text) => flatAltPaths[keyword] || text;59return numArgs > 060? str61.replace(replaceParams1, replaceFunc)62.replace(replaceParams2, replaceFunc)63: str.replace(replaceParams2, replaceFunc);64},65applyMassInsert = (str, flatPathObject, shouldIgnore = false) => {66const replaceParams = new RegExp(67`{{(${Object.keys(flatPathObject).join('|').replace(regExpEscape, '\\$&')})}}`,68'g'69),70replaceFunc = shouldIgnore71? (text, capture) => capture72: (text, capture) => flatPathObject[capture] || text;73return str.replace(replaceParams, replaceFunc);74},75ifSEO = (text) => (config.usingSEO ? text : ''),76ifDisguise = (text) => (config.disguiseFiles ? text : ''),77randomListItem = (lis) => () => lis[(Math.random() * lis.length) | 0],78getRandomChar = randomListItem(charRandom),79/* Text masks, found in src/data.json, are meant to be variations of the80* same term. Using a different term as a mask will break the spelling.81* HTML entities may also break if their names are used as terms.82*/83parsedTextMasks = Object.freeze(84Object.entries(textMasks).map((entry) => [85entry[0],86randomListItem(entry[1]),87entry[0].match(subtermsByCaps),88])89),90matchTextMasks = new RegExp(91Object.keys(textMasks)92.sort((term1, term2) => term2.length - term1.length)93.join('|')94.replace(regExpEscape, '\\$&'),95'gi'96),97maskTerm = (term) => {98if (config.usingSEO) return term;99const altList = parsedTextMasks.find(100(entry) => entry[0].toLowerCase() === term.toLowerCase()101);102let capitals = altList[2].map((word) => {103const letter = term[0];104term = term.slice(word.length);105return letter;106});107return altList[1]()108.replace(subtermsByCaps, (word) => capitals.shift() + word.slice(1))109.replaceAll(delimiter, getRandomChar);110},111mask = (text) =>112config.usingSEO113? text114: text115.replace(matchTextMasks, maskTerm)116.replace(termsBySpaces, (term) =>117containsMask.test(term)118? term119: term.replace(subtermsByVowels, getRandomChar)120),121route = (text, conditionalRoute = false) =>122conditionalRoute && routeConditions[conditionalRoute]123? text.replace(124getEndPoint,125(name) => cacheBustList[name] || flatAltPaths['files/' + name] || name126)127: text128.replace(129getEndPoint,130// cacheBustList is purely for dealing with cached file loading issues.131(name, ancestor) =>132ancestor133? flatAltPaths[name] || name134: flatAltPaths['files/' + name] ||135cacheBustList[name] ||136flatAltPaths[name] ||137name138)139.replace(140getPaths,141(path) =>142flatAltPaths['prefixes/' + path] || flatAltPaths[path] || path143)144.replace(getAbsoluteRoot, serverUrl.pathname),145inlineElement = (htmlStr) => {146let relPath = htmlStr.match(getRoutePath) || htmlStr.match(getAttrPath),147wrapper = [],148fileType;149if (relPath)150try {151relPath = new URL(152'../views/dist' +153new URL(relPath[0], 'https://www.example.com').pathname,154import.meta.url155);156fileType = relPath.pathname157.slice(relPath.pathname.lastIndexOf('.') + 1)158.toLowerCase();159switch (fileType) {160case 'css': {161wrapper = ['<style>', '</style>'];162break;163}164case 'js': {165const parsedNode = htmlStr166.replace(getAttrValues, ' ')167.toLowerCase();168if (169parsedNode.indexOf(' defer ') !== -1 ||170parsedNode.indexOf(' defer>') !== -1171)172wrapper = ['<script defer>', '</script>'];173else wrapper = ['<script>', '</script>'];174break;175}176default: {177// Do nothing.178}179}180} catch (e) {181relPath = '';182console.log(e);183}184return relPath && wrapper.length && existsSync(relPath)185? wrapper[0] +186readFileSync(relPath, 'utf8').trim() +187(wrapper[1] || wrapper[0])188: htmlStr;189},190inline = (htmlStr) =>191routeConditions.inline192? htmlStr.replace(getNodesByLine, inlineElement)193: htmlStr,194insertCharset = (str) => str.replace(charset, getRandomChar),195getSplash = () => randomListItem(splashRandom)(),196getCookingText = () =>197`<span style="display:none" data-fact="${randomListItem(vegetables)()}">${randomListItem(cookingInserts)()}</span>`,198insertCooking = (str) =>199str.replaceAll(200'<!-- IMPORTANT-HUCOOKINGINSERT-DONOTDELETE -->',201getCookingText202),203encodingTable = (() => {204let yummyOneBytes = '';205for (let i = 0; i < 128; i++)206if (207JSON.stringify(JSON.stringify(String.fromCodePoint(i)).slice(1, -1))208.length < 6209)210yummyOneBytes += String.fromCodePoint(i);211return yummyOneBytes;212})(),213createRandomID = () =>214crypto215.randomUUID()216.split('-')217.map((gibberish) => {218let randomNumber = parseInt(gibberish, 16),219output = '';220while (randomNumber >= encodingTable.length) {221output +=222encodingTable[Math.floor(randomNumber) % encodingTable.length];223randomNumber = randomNumber / encodingTable.length;224}225return output + Math.floor(randomNumber);226})227.join('')228.replaceAll('{', '')229.replaceAll('}', ''),230// To be used for {{insertions}} that are also encased in string literals.231escapeStr = (str) =>232str233.replace(basicStrEscape, '\\$&')234.replaceAll('\r', '\\r')235.replaceAll('\n', '\\n'),236orderedTransforms = [237[getSplash, 0],238[route, 2],239[route, 1],240[ifSEO, 1],241[ifDisguise, 1],242[mask, 1],243[inline, 1],244],245namedEntries = Object.freeze({246__uv$config: escapeStr(247config.randomizeIdentifiers ? createRandomID() : '__uv$config'248),249version: versionValue,250cacheVal: crypto.getRandomValues(new Uint32Array(1))[0],251defaultSearch: '{{DuckDuckGo}}',252}),253// List of manual censors for unavoidable cases.254manualCensors = Object.freeze({255Google: 'Google',256Bing: 'Bing',257Brave: 'Brave',258DuckDuckGo: 'DuckDuckGo',259Startpage: 'Startpage',260'wisp-transport': 'wst',261libcurl: 'unix',262epoxy: 'epoch',263'hu-lts': 'net-time',264}),265// Apply most obfuscation changes to an entire file's text content.266prePaint = (str) => {267let paintedSource = insertCharset(insertCooking(str));268paintedSource = applyMassInsert(269applyMassInsert(paintedSource, namedEntries),270manualCensors,271config.usingSEO272);273for (let i = 0, total = orderedTransforms.length; i < total; i++)274paintedSource = applyInsert(paintedSource, ...orderedTransforms[i]);275return paintedSource;276},277// Functionally similar to templates.mjs, but requires more situational formatting.278specialTemplates = Object.freeze({279'ultraviolet-error': escapeStr(prePaint(uvError)),280'scramjet-error': escapeStr(prePaint(sjError)),281}),282// Apply final changes to a given file's text content.283paintSource = (str) => applyMassInsert(prePaint(str), specialTemplates);284285286