mirror of
https://github.com/SEPPDROID/JellyCAT.git
synced 2025-10-22 07:54:27 +00:00
563 lines
18 KiB
JavaScript
563 lines
18 KiB
JavaScript
// /\_/|
|
|
// { ' ' } JellyCAT
|
|
// \____\
|
|
|
|
fetchSettings(function() {
|
|
overrideConsoleLog();
|
|
console.log("Successfully retrieved settings & overwritten console log (JCM), sending logs to JCATHOST now.")
|
|
});
|
|
|
|
// ***************************************************
|
|
// JellyCAT Logger | Jclogger
|
|
// Function to override console.log, console.error, and console.warn and send logs to the JellyCAT stHack server
|
|
|
|
function overrideConsoleLog() {
|
|
var originalConsoleLog = console.log;
|
|
var originalConsoleError = console.error;
|
|
var originalConsoleWarn = console.warn;
|
|
|
|
console.log = function () {
|
|
// Call the original console.log
|
|
originalConsoleLog.apply(console, arguments);
|
|
|
|
// Send the log to the server
|
|
logToServer("LOG: " + JSON.stringify(arguments));
|
|
};
|
|
|
|
console.error = function () {
|
|
// Call the original console.error
|
|
originalConsoleError.apply(console, arguments);
|
|
|
|
// Send the error to the server
|
|
logToServer("ERROR: " + JSON.stringify(arguments));
|
|
};
|
|
|
|
console.warn = function () {
|
|
// Call the original console.warn
|
|
originalConsoleWarn.apply(console, arguments);
|
|
|
|
// Send the warning to the server
|
|
logToServer("WARNING: " + JSON.stringify(arguments));
|
|
};
|
|
}
|
|
|
|
// Function to log console information to the server
|
|
// This however opens up a POI for attackers, so only leave this exposed in a dev environment. #todo: add in RM.MD
|
|
function logToServer(logData) {
|
|
var logEndpoint = "https://" + atv.jcathost.SigHost + "/log";
|
|
|
|
var logRequest = new XMLHttpRequest();
|
|
logRequest.open("POST", logEndpoint, true);
|
|
logRequest.setRequestHeader("Content-Type", "application/json");
|
|
|
|
var logPayload = {
|
|
timestamp: new Date().toISOString(),
|
|
logData: logData
|
|
};
|
|
|
|
logRequest.send(JSON.stringify(logPayload));
|
|
}
|
|
|
|
// ***************************************************
|
|
// JellyCAT Host fetcher
|
|
// Function to fetch information we need from the host server
|
|
// fetch JSON from the HTTP URL using XMLHttpRequest
|
|
|
|
function fetchSettings(callback) {
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open('GET', 'http://jcathost.dns/atvcsettings', true);
|
|
|
|
xhr.onreadystatechange = function () {
|
|
if (xhr.readyState === 4) {
|
|
if (xhr.status === 200) {
|
|
try {
|
|
var data = JSON.parse(xhr.responseText);
|
|
|
|
// Store all properties in atv.jcathost
|
|
atv.jcathost = {
|
|
SigHost: data.sig_host,
|
|
SigHostPort: data.sig_host_p,
|
|
HostIP: data.host_ip,
|
|
System: data.system,
|
|
Version: data.version,
|
|
HelloMessage: data.hello,
|
|
// Add other properties as needed
|
|
};
|
|
|
|
// Execute the callback after setting atv.jcathost
|
|
callback();
|
|
} catch (jsonError) {
|
|
console.error('Error parsing JSON:', jsonError);
|
|
}
|
|
} else {
|
|
console.error('Error fetching settings. Status:', xhr.status);
|
|
}
|
|
}
|
|
};
|
|
|
|
xhr.send();
|
|
}
|
|
|
|
// ***************************************************
|
|
// ATVUtils - a JavaScript helper library for Apple TV
|
|
// Copied & Edited in full from:
|
|
// https://kortv.com/appletv/js/application.js
|
|
// https://github.com/wahlmanj/sample-aTV/blob/master/js/application.js
|
|
|
|
var atvutils = ATVUtils = {
|
|
makeRequest: function(url, method, headers, body, callback) {
|
|
if ( !url ) {
|
|
throw "loadURL requires a url argument";
|
|
}
|
|
|
|
var method = method || "GET",
|
|
headers = headers || {},
|
|
body = body || "";
|
|
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.onreadystatechange = function() {
|
|
try {
|
|
if (xhr.readyState == 4 ) {
|
|
if ( xhr.status == 200) {
|
|
callback(xhr.responseXML);
|
|
} else {
|
|
console.log("makeRequest received HTTP status " + xhr.status + " for " + url);
|
|
callback(null);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('makeRequest caught exception while processing request for ' + url + '. Aborting. Exception: ' + e);
|
|
xhr.abort();
|
|
callback(null);
|
|
}
|
|
}
|
|
xhr.open(method, url, true);
|
|
|
|
for(var key in headers) {
|
|
xhr.setRequestHeader(key, headers[key]);
|
|
}
|
|
|
|
xhr.send();
|
|
return xhr;
|
|
},
|
|
|
|
makeErrorDocument: function(message, description) {
|
|
if ( !message ) {
|
|
message = "";
|
|
}
|
|
if ( !description ) {
|
|
description = "";
|
|
}
|
|
|
|
var errorXML = '<?xml version="1.0" encoding="UTF-8"?> \
|
|
<atv> \
|
|
<body> \
|
|
<dialog id="com.sample.error-dialog"> \
|
|
<title><![CDATA[' + message + ']]></title> \
|
|
<description><![CDATA[' + description + ']]></description> \
|
|
</dialog> \
|
|
</body> \
|
|
</atv>';
|
|
|
|
return atv.parseXML(errorXML);
|
|
},
|
|
|
|
siteUnavailableError: function() {
|
|
// TODO: localize
|
|
return this.makeErrorDocument("JellyCAT is currently unavailable. Try again later.", "Check JCHOST for log information.");
|
|
},
|
|
|
|
loadError: function(message, description) {
|
|
atv.loadXML(this.makeErrorDocument(message, description));
|
|
},
|
|
|
|
loadAndSwapError: function(message, description) {
|
|
atv.loadAndSwapXML(this.makeErrorDocument(message, description));
|
|
},
|
|
|
|
loadURLInternal: function(url, method, headers, body, loader) {
|
|
var me = this,
|
|
xhr,
|
|
proxy = new atv.ProxyDocument;
|
|
|
|
proxy.show();
|
|
|
|
proxy.onCancel = function() {
|
|
if ( xhr ) {
|
|
xhr.abort();
|
|
}
|
|
};
|
|
|
|
xhr = me.makeRequest(url, method, headers, body, function(xml) {
|
|
try {
|
|
loader(proxy, xml);
|
|
} catch(e) {
|
|
console.error("Caught exception in for " + url + ". " + e);
|
|
loader(me.siteUnavailableError());
|
|
}
|
|
});
|
|
},
|
|
|
|
loadURL: function( options ) { //url, method, headers, body, processXML) {
|
|
var me = this;
|
|
if( typeof( options ) === "string" ) {
|
|
var url = options;
|
|
} else {
|
|
var url = options.url,
|
|
method = options.method || null,
|
|
headers = options.headers || null,
|
|
body = options.body || null,
|
|
processXML = options.processXML || null;
|
|
}
|
|
|
|
this.loadURLInternal(url, method, headers, body, function(proxy, xml) {
|
|
if(typeof(processXML) == "function") processXML.call(this, xml);
|
|
try {
|
|
proxy.loadXML(xml, function(success) {
|
|
if ( !success ) {
|
|
console.log("loadURL failed to load " + url);
|
|
proxy.loadXML(me.siteUnavailableError());
|
|
}
|
|
});
|
|
} catch (e) {
|
|
console.log("loadURL caught exception while loading " + url + ". " + e);
|
|
proxy.loadXML(me.siteUnavailableError());
|
|
}
|
|
});
|
|
},
|
|
|
|
// loadAndSwapURL can only be called from page-level JavaScript of the page that wants to be swapped out.
|
|
loadAndSwapURL: function( options ) { //url, method, headers, body, processXML) {
|
|
var me = this;
|
|
if( typeof( options ) === "string" ) {
|
|
var url = options;
|
|
} else {
|
|
var url = options.url,
|
|
method = options.method || null,
|
|
headers = options.headers || null,
|
|
body = options.body || null,
|
|
processXML = options.processXML || null;
|
|
}
|
|
|
|
this.loadURLInternal(url, method, headers, body, function(proxy, xml) {
|
|
if(typeof(processXML) == "function") processXML.call(this, xml);
|
|
try {
|
|
proxy.loadXML(xml, function(success) {
|
|
if ( success ) {
|
|
atv.unloadPage();
|
|
} else {
|
|
console.log("loadAndSwapURL failed to load " + url);
|
|
proxy.loadXML(me.siteUnavailableError(), function(success) {
|
|
if ( success ) {
|
|
atv.unloadPage();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
} catch (e) {
|
|
console.error("loadAndSwapURL caught exception while loading " + url + ". " + e);
|
|
proxy.loadXML(me.siteUnavailableError(), function(success) {
|
|
if ( success ) {
|
|
atv.unloadPage();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Used to manage setting and retrieving data from local storage
|
|
*/
|
|
data: function(key, value) {
|
|
if(key && value) {
|
|
try {
|
|
atv.localStorage.setItem(key, value);
|
|
return value;
|
|
} catch(error) {
|
|
console.error('Failed to store data element: '+ error);
|
|
}
|
|
|
|
} else if(key) {
|
|
try {
|
|
return atv.localStorage.getItem(key);
|
|
} catch(error) {
|
|
console.error('Failed to retrieve data element: '+ error);
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
deleteData: function(key) {
|
|
try {
|
|
atv.localStorage.removeItem(key);
|
|
} catch(error) {
|
|
console.error('Failed to remove data element: '+ error);
|
|
}
|
|
},
|
|
|
|
|
|
/**
|
|
* @params options.name - string node name
|
|
* @params options.text - string textContent
|
|
* @params options.attrs - array of attribute to set {"name": string, "value": string, bool}
|
|
* @params options.children = array of childNodes same values as options
|
|
* @params doc - document to attach the node to
|
|
* returns node
|
|
*/
|
|
createNode: function(options, doc) {
|
|
var doc = doc || document;
|
|
options = options || {};
|
|
|
|
if(options.name && options.name != '') {
|
|
var newElement = doc.makeElementNamed(options.name);
|
|
|
|
if(options.text) newElement.textContent = options.text;
|
|
|
|
if(options.attrs) {
|
|
options.attrs.forEach(function(e, i, a) {
|
|
newElement.setAttribute(e.name, e.value);
|
|
}, this);
|
|
}
|
|
|
|
if(options.children) {
|
|
options.children.forEach(function(e,i,a) {
|
|
newElement.appendChild( this.createNode( e, doc ) );
|
|
}, this)
|
|
}
|
|
|
|
return newElement;
|
|
}
|
|
},
|
|
|
|
validEmailAddress: function( email ) {
|
|
var emailRegex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
|
|
isValid = email.search( emailRegex );
|
|
return ( isValid > -1 );
|
|
},
|
|
|
|
softwareVersionIsAtLeast: function( version ) {
|
|
var deviceVersion = atv.device.softwareVersion.split('.'),
|
|
requestedVersion = version.split('.');
|
|
|
|
// We need to pad the device version length with "0" to account for 5.0 vs 5.0.1
|
|
if( deviceVersion.length < requestedVersion.length ) {
|
|
var difference = requestedVersion.length - deviceVersion.length,
|
|
dvl = deviceVersion.length;
|
|
|
|
for( var i = 0; i < difference; i++ ) {
|
|
deviceVersion[dvl + i] = "0";
|
|
};
|
|
};
|
|
|
|
// compare the same index from each array.
|
|
for( var c = 0; c < deviceVersion.length; c++ ) {
|
|
var dv = deviceVersion[c],
|
|
rv = requestedVersion[c] || "0";
|
|
|
|
if( parseInt( dv ) > parseInt( rv ) ) {
|
|
return true;
|
|
} else if( parseInt( dv ) < parseInt( rv ) ) {
|
|
return false;
|
|
};
|
|
};
|
|
|
|
// If we make it this far the two arrays are identical, so we're true
|
|
return true;
|
|
},
|
|
|
|
shuffleArray: function( arr ) {
|
|
var tmp, current, top = arr.length;
|
|
|
|
if(top) {
|
|
while(--top) {
|
|
current = Math.floor(Math.random() * (top + 1));
|
|
tmp = arr[current];
|
|
arr[current] = arr[top];
|
|
arr[top] = tmp;
|
|
};
|
|
};
|
|
|
|
return arr;
|
|
},
|
|
|
|
loadTextEntry: function( textEntryOptions ) {
|
|
var textView = new atv.TextEntry;
|
|
|
|
textView.type = textEntryOptions.type || "emailAddress";
|
|
textView.title = textEntryOptions.title || "";
|
|
textView.image = textEntryOptions.image || null;
|
|
textView.instructions = textEntryOptions.instructions || "";
|
|
textView.label = textEntryOptions.label || "";
|
|
textView.footnote = textEntryOptions.footnote || "";
|
|
textView.defaultValue = textEntryOptions.defaultValue || null;
|
|
textView.defaultToAppleID = textEntryOptions.defaultToAppleID || false;
|
|
textView.onSubmit = textEntryOptions.onSubmit,
|
|
textView.onCancel = textEntryOptions.onCancel,
|
|
|
|
textView.show();
|
|
},
|
|
|
|
log: function ( message , level ) {
|
|
var debugLevel = atv.sessionStorage.getItem( "DEBUG_LEVEL" ),
|
|
level = level || 0;
|
|
|
|
if( level <= debugLevel ) {
|
|
console.log( message );
|
|
}
|
|
},
|
|
|
|
accessibilitySafeString: function ( string ) {
|
|
var string = unescape( string );
|
|
|
|
string = string
|
|
.replace( /&/g, 'and' )
|
|
.replace( /&/g, 'and' )
|
|
.replace( /</g, 'less than' )
|
|
.replace( /\</g, 'less than' )
|
|
.replace( />/g, 'greater than' )
|
|
.replace( /\>/g, 'greater than' );
|
|
|
|
return string;
|
|
}
|
|
};
|
|
|
|
// Extend atv.ProxyDocument to load errors from a message and description.
|
|
if( atv.ProxyDocument ) {
|
|
atv.ProxyDocument.prototype.loadError = function(message, description) {
|
|
var doc = atvutils.makeErrorDocument(message, description);
|
|
this.loadXML(doc);
|
|
}
|
|
}
|
|
|
|
|
|
// atv.Document extensions
|
|
if( atv.Document ) {
|
|
atv.Document.prototype.getElementById = function(id) {
|
|
var elements = this.evaluateXPath("//*[@id='" + id + "']", this);
|
|
if ( elements && elements.length > 0 ) {
|
|
return elements[0];
|
|
}
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
|
|
// atv.Element extensions
|
|
if( atv.Element ) {
|
|
atv.Element.prototype.getElementsByTagName = function(tagName) {
|
|
return this.ownerDocument.evaluateXPath("descendant::" + tagName, this);
|
|
}
|
|
|
|
atv.Element.prototype.getElementByTagName = function(tagName) {
|
|
var elements = this.getElementsByTagName(tagName);
|
|
if ( elements && elements.length > 0 ) {
|
|
return elements[0];
|
|
}
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
// Simple Array Sorting methods
|
|
Array.prototype.sortAsc = function() {
|
|
this.sort(function( a, b ){
|
|
return a - b;
|
|
});
|
|
};
|
|
|
|
Array.prototype.sortDesc = function() {
|
|
this.sort(function( a, b ){
|
|
return b - a;
|
|
});
|
|
};
|
|
|
|
|
|
// Date methods and properties
|
|
Date.lproj = {
|
|
"DAYS": {
|
|
"en": {
|
|
"full": ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
|
|
"abbrv": ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
},
|
|
"en_GB": {
|
|
"full": ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
|
|
"abbrv": ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
}
|
|
},
|
|
"MONTHS": {
|
|
"en": {
|
|
"full": ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
|
"abbrv": ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
|
},
|
|
"en_GB": {
|
|
"full": ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
|
"abbrv": ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
|
}
|
|
}
|
|
}
|
|
|
|
Date.prototype.getLocaleMonthName = function( type ) {
|
|
var language = atv.device.language,
|
|
type = ( type === true ) ? "abbrv" : "full",
|
|
MONTHS = Date.lproj.MONTHS[ language ] || Date.lproj.MONTHS[ "en" ];
|
|
|
|
return MONTHS[ type ][ this.getMonth() ];
|
|
};
|
|
|
|
Date.prototype.getLocaleDayName = function( type ) {
|
|
var language = atv.device.language,
|
|
type = ( type === true ) ? "abbrv" : "full",
|
|
DAYS = Date.lproj.DAYS[ language ] || Date.lproj.DAYS[ "en" ];
|
|
|
|
return DAYS[ type ][ this.getDay() ];
|
|
};
|
|
|
|
Date.prototype.nextDay = function( days ) {
|
|
var oneDay = 86400000,
|
|
days = days || 1;
|
|
this.setTime( new Date( this.valueOf() + ( oneDay * days ) ) );
|
|
};
|
|
|
|
Date.prototype.prevDay = function( days ) {
|
|
var oneDay = 86400000,
|
|
days = days || 1;
|
|
this.setTime( new Date( this.valueOf() - ( oneDay * days ) ) );
|
|
};
|
|
|
|
|
|
// String Trim methods
|
|
String.prototype.trim = function ( ch )
|
|
{
|
|
var ch = ch || '\\s',
|
|
s = new RegExp( '^['+ch+']+|['+ch+']+$','g');
|
|
return this.replace(s,'');
|
|
};
|
|
|
|
String.prototype.trimLeft = function ( ch )
|
|
{
|
|
var ch = ch || '\\s',
|
|
s = new RegExp( '^['+ch+']+','g');
|
|
return this.replace(s,'');
|
|
};
|
|
|
|
String.prototype.trimRight = function ( ch )
|
|
{
|
|
var ch = ch || '\\s',
|
|
s = new RegExp( '['+ch+']+$','g');
|
|
return this.replace(s,'');
|
|
};
|
|
|
|
String.prototype.xmlEncode = function()
|
|
{
|
|
var string = unescape( this );
|
|
|
|
string = string
|
|
.replace( /&/g, '&' )
|
|
.replace( /\</g, '<' )
|
|
.replace( /\>/g, '>' );
|
|
|
|
return string;
|
|
};
|
|
|
|
console.log('Reached EOF!');
|
|
|