Some testing & cleanup

This commit is contained in:
2024-02-09 20:34:55 +01:00
parent 1be97536ef
commit fcb3642ac5
19 changed files with 957 additions and 161 deletions

2
.gitignore vendored
View File

@@ -3,5 +3,3 @@
/assets/certificates/certificate.cer /assets/certificates/certificate.cer
/assets/certificates/certificate.pem /assets/certificates/certificate.pem
/assets/certificates/private.key /assets/certificates/private.key
/app/assets/img/icons/jcat@720.png
/app/assets/img/icons/jcat@1080.png

2
app.go
View File

@@ -112,7 +112,7 @@ func displayCinfo() {
fmt.Println("| |") fmt.Println("| |")
fmt.Println("| *** WEB *** |") fmt.Println("| *** WEB *** |")
fmt.Println("| |") fmt.Println("| |")
fmt.Println("| https_port = ", config.HttpsPort, " |") fmt.Println("| https_port = ", config.HttpsPort, " |")
fmt.Println("| https_port = ", config.HttpPort, " |") fmt.Println("| https_port = ", config.HttpPort, " |")
fmt.Println("| |") fmt.Println("| |")
fmt.Println("=========================================") fmt.Println("=========================================")

View File

@@ -10,8 +10,7 @@ atv.onAppEntry = function () {
fetchSettings(function() { fetchSettings(function() {
overrideConsoleLog(); overrideConsoleLog();
console.log("Successfully overwritten console log, sending logs to JCATHOST now.") console.log("Successfully overwritten console log, sending logs to JCATHOST now.")
console.log("Received ATVCSETTINGS From JCATHOST."); console.log("Received ATVCSETTINGS From JCATHOST. And starting JellyCAT-APP-JS on AppleTV...");
console.log("Starting JellyCAT-JS on AppleTV...")
jcatMain(); jcatMain();
}); });
} }
@@ -22,16 +21,16 @@ atv.onAppExit = function() {
// *************************************************** // ***************************************************
// JellyCAT Main | Main JS app function // JellyCAT Main | Main JS app function
// Help // Help I'm even worse at JS
function jcatMain(){ function jcatMain(){
atvutils.loadURL("https://" + atv.jcathost.SigHost + "/xml/home.xml"); atvutils.loadURL("https://" + atv.jcathost.SigHost + "/xml/devtools.xml");
} }
// *************************************************** // ***************************************************
// JellyCAT Logger | Jclogger // JellyCAT Logger | Jclogger
// Function to override console.log, console.error, and console.warn and send logs to the JellyCAT stHack server // Function to override console.log, console.error, and console.warn and send logs to the JellyCAT stHack server
// We shall never send any sensitive information!!
function overrideConsoleLog() { function overrideConsoleLog() {
var originalConsoleLog = console.log; var originalConsoleLog = console.log;
var originalConsoleError = console.error; var originalConsoleError = console.error;
@@ -81,7 +80,7 @@ function logToServer(logData) {
// *************************************************** // ***************************************************
// JellyCAT Host fetcher // JellyCAT Host fetcher
// Function to fetch information we need from the host server // Function to fetch information we need from the host server
// Function to fetch JSON from the HTTP URL using XMLHttpRequest // fetch JSON from the HTTP URL using XMLHttpRequest
function fetchSettings(callback) { function fetchSettings(callback) {
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
app/assets/img/jctools.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -1,56 +0,0 @@
// /\_/|
// { ' ' } JellyCAT
// \____\
overrideConsoleLog();
console.log("Successfully overwritten console log, 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
// We shall never send any sensitive information!!
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
function logToServer(logData) {
var logEndpoint = "http://jcathost.dns/log"; // insecure for now
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));
}

562
app/js/jcm.js Normal file
View File

@@ -0,0 +1,562 @@
// /\_/|
// { ' ' } 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( /&amp;/g, 'and' )
.replace( /&/g, 'and' )
.replace( /&lt;/g, 'less than' )
.replace( /\</g, 'less than' )
.replace( /&gt;/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, '&amp;' )
.replace( /\</g, '&lt;' )
.replace( /\>/g, '&gt;' );
return string;
};
console.log('Reached EOF!');

205
app/js/jsvtestpage.js Normal file
View File

@@ -0,0 +1,205 @@
/*
* https://github.com/wahlmanj/sample-aTV/blob/dba73806c21183fb35d6edca94b960691d8e5d66/js/views/text-view/text-view.js
* text-view.js - Demonstrates atv.TextView
* TextView's have an attributedString parameter that is an Object with two required properties: string and attributes.
* - string is the string displayed in the text view on screen.
* - attributes is an Object with two required properties (pointSize and color) and optional properties (alignment, weight, and breakMode).
*
* attribute Object properties
* pointSize: Required. A number that is the size of the font in points
* color: Required. An object with 3 required properties (red, green, and blue) and one optional property (alpha). Each of the color components is a number from 0 to 1.
* alignment: Optional. left, right, center, or justify
* weight: Optional. light, normal, or heavy
* breakMode: Optional. clip, word-wrap, truncate-head, truncate-tail, or truncate-middle.
clip: the text will be displayed on a single line and cut off at the end of the text view
word-wrap: the text will word wrap until there is no more space, at which point it is clipped to the text view
truncate-head: the text will be displayed on a single line with an ellipsis at the beginning if the string would be clipped otherwise
truncate-tail: the text will be displayed on a single line with an ellipsis at the end if the string would be clipped otherwise
truncate-middle: the text will be displayed on a single line with an ellipsis in the middle if the string would be clipped otherwise
*/
var loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ut arcu ut lectus pellentesque ultrices. Sed eu neque et nisl feugiat interdum. Praesent rutrum magna in massa consectetur semper. Nullam gravida odio sit amet purus mattis porta. Sed in est suscipit ipsum imperdiet dapibus. Curabitur cursus fermentum lectus, quis aliquam magna convallis vitae. Maecenas egestas molestie rhoncus. Morbi erat neque, egestas quis condimentum quis, mattis a est. Maecenas vel ipsum a dui posuere tincidunt. Suspendisse tincidunt posuere pretium. Sed in quam eget nibh ultricies dictum. Aliquam est lorem, vestibulum nec congue id, tincidunt vel nunc. Nulla in sem massa, a fermentum purus. In hac habitasse platea dictumst. Vestibulum euismod iaculis justo id rhoncus.";
var screenFrame = atv.device.screenFrame;
var containerView = new atv.View();
var exampleTextViews = [];
containerView.frame = screenFrame;
var startY = screenFrame.height * 0.05;
var currentY = startY;
var hPadding = screenFrame.width * 0.03;
var currentX = hPadding;
var height = screenFrame.height * 0.12;
var width = screenFrame.width * 0.25;
function addTextView () {
var textView = new atv.TextView();
textView.backgroundColor = { red: 0.2, blue: 0.2, green: 0.2 };
var vSpacing = screenFrame.height * 0.01;
textView.frame = {
x: currentX,
y: currentY,
width: width,
height: height
};
currentY += height + vSpacing;
if ( currentY + height + vSpacing > containerView.frame.height )
{
currentY = startY;
currentX += width + hPadding;
}
exampleTextViews.push(textView);
return textView;
}
//
// TextView using only the required attributes
//
var onlyRequiredAttributesView = addTextView();
onlyRequiredAttributesView.attributedString = {
string: "Only Required Attributes: " + loremIpsum,
attributes: {
// Required attributes
pointSize: 20.0,
color: { red: 1, blue: 1, green: 1 }
}
};
//
// TextView's demonstrating alignments
//
var rightAlignmentView = addTextView();
rightAlignmentView.attributedString = {
string: "Right Alignment",
attributes: {
// Required attributes
pointSize: 20.0,
color: { red: 1, blue: 1, green: 1 },
alignment: "right"
}
};
var centerAlignmentView = addTextView();
centerAlignmentView.attributedString = {
string: "Center Alignment",
attributes: {
// Required attributes
pointSize: 20.0,
color: { red: 1, blue: 1, green: 1 },
alignment: "center"
}
};
var leftAlignmentView = addTextView();
leftAlignmentView.attributedString = {
string: "Left Alignment",
attributes: {
// Required attributes
pointSize: 20.0,
color: { red: 1, blue: 1, green: 1 },
alignment: "left"
}
};
//
// TextView's demonstrating weights
//
var normalWeight = addTextView();
normalWeight.attributedString = {
string: "Normal Weight",
attributes: {
// Required attributes
pointSize: 40.0,
color: { red: 1, blue: 1, green: 1 },
weight: "normal"
}
};
var heavyWeight = addTextView();
heavyWeight.attributedString = {
string: "Heavy Weight",
attributes: {
// Required attributes
pointSize: 40.0,
color: { red: 1, blue: 1, green: 1 },
weight: "heavy"
}
};
var lightWeight = addTextView();
lightWeight.attributedString = {
string: "Light Weight",
attributes: {
// Required attributes
pointSize: 40.0,
color: { red: 1, blue: 1, green: 1 },
weight: "light"
}
};
//
// TextView's demonstrating breakModes
//
var clip = addTextView();
clip.attributedString = {
string: "clip: " + loremIpsum,
attributes: {
// Required attributes
pointSize: 20.0,
color: { red: 1, blue: 1, green: 1 },
breakMode: "clip"
}
};
var wordWrap = addTextView();
wordWrap.attributedString = {
string: "word-wrap: " + loremIpsum,
attributes: {
// Required attributes
pointSize: 20.0,
color: { red: 1, blue: 1, green: 1 },
breakMode: "word-wrap"
}
};
var truncateHead = addTextView();
truncateHead.attributedString = {
string: loremIpsum + " truncate-head",
attributes: {
// Required attributes
pointSize: 20.0,
color: { red: 1, blue: 1, green: 1 },
breakMode: "truncate-head"
}
};
var truncateTail = addTextView();
truncateTail.attributedString = {
string: "truncate-tail: " + loremIpsum,
attributes: {
// Required attributes
pointSize: 20.0,
color: { red: 1, blue: 1, green: 1 },
breakMode: "truncate-tail"
}
};
var truncateMiddle = addTextView();
truncateMiddle.attributedString = {
string: "truncate-middle: " + loremIpsum,
attributes: {
// Required attributes
pointSize: 20.0,
color: { red: 1, blue: 1, green: 1 },
breakMode: "truncate-middle"
}
};
containerView.subviews = exampleTextViews;
controller.view = containerView;

45
app/js/storagetest.js Normal file
View File

@@ -0,0 +1,45 @@
// /\_/|
// { ' ' } JellyCAT
// \____\
function printSessionStorage() {
var keys = ['test', 'exampleKey2', 'exampleKey3'];
keys.forEach(function(key) {
var value = atv.sessionStorage.getItem(key);
if (value !== null) {
console.log(key + ": " + value);
} else {
console.log("No value found for key: " + key);
}
});
}
function printLocalStorage() {
var keys = ['test', 'exampleKey2', 'exampleKey3'];
keys.forEach(function(key) {
var value = atv.localStorage.getItem(key);
if (value !== null) {
console.log(key + ": " + value);
} else {
console.log("No value found for key: " + key);
}
});
}
function setTestSessionStorageItem() {
// Set an example item in sessionStorage
atv.sessionStorage.setItem('test', 'exampleValueForSessionStorage');
console.log('Test item set in sessionStorage.');
}
function setTestLocalStorageItem() {
// Set an example item in sessionStorage
atv.localStorage.setItem('test', 'exampleValueForLocalStorage');
console.log('Test item set in localStorage.');
}

43
app/xml/devtools.xml Normal file
View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<atv>
<head>
<script src="http://jcathost.dns/js/jcm.js"/>
</head>
<body>
<optionDialog id="com.jellycat.devtools">
<header>
<simpleHeader accessibilityLabel="Dialog with Options">
<title>JellyCAT Debug/Dev/Test Tools</title>
<image required="true">http://jcathost.dns/assets/img/jctools.png</image>
</simpleHeader>
</header>
<description>These options are intended for testing your development environment only. It is crucial to refrain from utilizing these test functions on server hosts that are not under your management, as doing so may inadvertently expose sensitive data to the server's administrator.</description>
<menu>
<initialSelection>
<row>4</row>
</initialSelection>
<sections>
<menuSection>
<items>
<oneLineMenuItem id="list_0" accessibilityLabel="Option 1" onSelect="console.log('log information sent');">
<label>Send Test log</label>
</oneLineMenuItem>
<oneLineMenuItem id="list_1" accessibilityLabel="Option 2" onSelect="console.log('loading test-page'); atvutils.loadURL('https://'+ atv.jcathost.SigHost +'/xml/dvt/testpage.xml');">
<label>Load XML Test Page</label>
</oneLineMenuItem>
<oneLineMenuItem id="list_2" accessibilityLabel="Option 3" onSelect="console.log('loading JS-VIEW test-page'); atvutils.loadURL('https://'+ atv.jcathost.SigHost +'/xml/dvt/jsvtestpage.xml');">
<label>Load JS-VIEW Test Page</label>
</oneLineMenuItem>
<oneLineMenuItem id="list_3" accessibilityLabel="Option 4" onSelect="console.log('loading storage-test-page'); atvutils.loadURL('https://'+ atv.jcathost.SigHost +'/xml/dvt/storagetest.xml');">
<label>Load Storage Testing</label>
</oneLineMenuItem>
<oneLineMenuItem id="list_4" accessibilityLabel="Option 5" onSelect="console.log('Unloading page'); atv.unloadPage();">
<label>Unload Page (go back)</label>
</oneLineMenuItem>
</items>
</menuSection>
</sections>
</menu>
</optionDialog>
</body>
</atv>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<atv>
<body>
<scriptView id="com.jellycat.jsctestpage">
<script src="http://jcathost.dns/js/jsvtestpage.js"/>
</scriptView>
</body>
</atv>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<atv>
<head>
<script src="http://jcathost.dns/js/jcm.js"/>
<script src="http://jcathost.dns/js/storagetest.js"/>
</head>
<body>
<optionList id="com.jellycat.storagetestpage" autoSelectSingleItem="false">
<footnote>atv.localstorage || storage data elements || JellyCAT</footnote>
<items>
<oneLineMenuItem id="list_0" accessibilityLabel="Set test session storage key" onSelect="setTestSessionStorageItem();">
<label>Set ATV APP session storage key (test)</label>
</oneLineMenuItem>
<oneLineMenuItem id="list_0" accessibilityLabel="Set test local storage key" onSelect="setTestLocalStorageItem();">
<label>Set ATV APP local storage key (test)</label>
</oneLineMenuItem>
<oneLineMenuItem id="list_1" accessibilityLabel="Print session storage (log)" onSelect="printSessionStorage();">
<label>Print ATV App session storage (log)</label>
</oneLineMenuItem>
<oneLineMenuItem id="list_2" accessibilityLabel="Print ATV storage (log)" onSelect="printLocalStorage();">
<label>Print ATV App local storage (log)</label>
</oneLineMenuItem>
<oneLineMenuItem id="list_3" accessibilityLabel="Clear ATV Storage and exit" onSelect="console.log('CLEARING ALL APP STORAGE AND EXITING APP'); atv.localStorage.clear(); atv.sessionStorage.clear(); atv.exitApp();">
<label>Clear ATV App Storage and exit App</label>
</oneLineMenuItem>
</items>
</optionList>
</body>
</atv>

9
app/xml/dvt/testpage.xml Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<atv>
<body>
<dialog id="com.jellycat.xmltestpage">
<title>XML Test-Page Not Implemented</title>
<description>The feed for this page has not been written. (but works)</description>
</dialog>
</body>
</atv>

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<atv>
<head>
<script src="http://jcathost.dns/js/jclog.js"/>
</head>
<body>
<optionDialog id="com.sample.error-dialog">
<header>
<simpleHeader accessibilityLabel="Dialog with Options">
<title>JellyCAT Debug/Dev Tools</title>
</simpleHeader>
</header>
<description>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</description>
<menu>
<initialSelection>
<row>1</row>
</initialSelection>
<sections>
<menuSection>
<items>
<oneLineMenuItem id="list_0" accessibilityLabel="Option 1" onSelect="console.log('log information sent');">
<label>Send Test log</label>
</oneLineMenuItem>
<oneLineMenuItem id="list_0" accessibilityLabel="Option 2" onSelect="console.log('Option 2 selected'); atv.unloadPage();">
<label>Unload Page</label>
</oneLineMenuItem>
</items>
</menuSection>
</sections>
</menu>
</optionDialog>
</body>
</atv>

View File

@@ -13,20 +13,17 @@ import (
) )
func dnsResolver() { func dnsResolver() {
// Setting up the dns server to listen on 53, currently hardcoded because that's the default dns port.
server := &dns.Server{Addr: ":53", Net: "udp"} server := &dns.Server{Addr: ":53", Net: "udp"}
dns.HandleFunc(".", handleDNSRequest) dns.HandleFunc(".", handleDNSRequest)
go func() { go func() {
if err := server.ListenAndServe(); err != nil { if err := server.ListenAndServe(); err != nil {
fmt.Println("DNS.SERVER-ERR: Failed to start DNS server:", err) fmt.Println("DNS.SERVER-ERR: Failed to start DNS server:", err)
// Just exit if it errors, since it's the main function of using this "sthack"
os.Exit(1) os.Exit(1)
} }
}() }()
fmt.Println("DNS.SERVER-LOG: DNS server is ready and listening on *:53") fmt.Println("DNS.SERVER-LOG: DNS server is ready and listening on *:53")
} }
func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) { func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
@@ -35,32 +32,46 @@ func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
m.Compress = false m.Compress = false
for _, q := range r.Question { for _, q := range r.Question {
if (q.Name == config.HijackApp || q.Name == config.HijackImg || q.Name == "jcathost.dns.") && (q.Qtype == dns.TypeA || q.Qtype == dns.TypeAAAA) { if shouldHijack(q.Name, q.Qtype) {
// Resolve hijack domain app to the IP addresses from the config handleHijackedRequest(q.Name)
// Had to add some trippy quadA detection, since ipv6 (AAAA) requests would end up funky addHijackedAnswer(m, q.Name)
fmt.Print("\033[A\r")
fmt.Println("DNS.SERVER-LOG: DNS LOOKUP Hijacked for", q.Name)
resetCommand()
rr, err := dns.NewRR(fmt.Sprintf("%s IN A %s", q.Name, config.HijackIP))
if err == nil {
m.Answer = append(m.Answer, rr)
}
} else { } else {
// Forward other requests to the IP address from the config forwardRequest(m, r)
resolver := &dns.Client{}
resp, _, err := resolver.Exchange(r, net.JoinHostPort(config.ForwardIP, "53"))
fmt.Print("\033[A\r")
fmt.Println("DNS.SERVER-LOG: DNS LOOKUP forwarded for", q.Name, "| uninterested")
resetCommand()
if err == nil {
m.Answer = append(m.Answer, resp.Answer...)
}
} }
} }
// Not sure if this is correct, my IDE autocorrect wanted to do it this way...
err := w.WriteMsg(m) err := w.WriteMsg(m)
if err != nil { if err != nil {
return fmt.Print("\033[A\r")
fmt.Println("DNS.SERVER-ERR: Error writing response:", err)
resetCommand()
} }
} }
func shouldHijack(name string, qtype uint16) bool {
return (name == config.HijackApp || name == config.HijackImg || name == "jcathost.dns.") && (qtype == dns.TypeA || qtype == dns.TypeAAAA)
}
func handleHijackedRequest(name string) {
fmt.Print("\033[A\r")
fmt.Println("DNS.SERVER-LOG: DNS LOOKUP Hijacked for", name)
resetCommand()
}
func addHijackedAnswer(m *dns.Msg, name string) {
rr, err := dns.NewRR(fmt.Sprintf("%s IN A %s", name, config.HijackIP))
if err == nil {
m.Answer = append(m.Answer, rr)
}
}
func forwardRequest(m *dns.Msg, r *dns.Msg) {
resolver := &dns.Client{}
resp, _, err := resolver.Exchange(r, net.JoinHostPort(config.ForwardIP, "53"))
if err == nil {
m.Answer = append(m.Answer, resp.Answer...)
}
fmt.Print("\033[A\r")
fmt.Println("DNS.SERVER-LOG: DNS LOOKUP forwarded for", r.Question[0].Name, "| uninterested")
resetCommand()
}

View File

@@ -26,7 +26,7 @@ func main() {
// Default information store // Default information store
JellyCAT = JcatDefaults{ JellyCAT = JcatDefaults{
Version: "0.1.1revB", Version: "0.1.1revC",
Name: "JellyCAT Serving stHack", Name: "JellyCAT Serving stHack",
HostName: config.CertName, HostName: config.CertName,
HostIP: config.HijackIP, HostIP: config.HijackIP,

View File

@@ -13,7 +13,7 @@
hijack_ip = "192.168.11.23" hijack_ip = "192.168.11.23"
hijack_app = "atv.qello.com." hijack_app = "atv.qello.com."
hijack_img = "notused.tld." hijack_img = "notused.com."
forward_ip = "1.1.1.1" forward_ip = "1.1.1.1"
# WEBServer Settings # WEBServer Settings

View File

@@ -26,30 +26,20 @@ type JcLogEntry struct {
} }
func webServer() { func webServer() {
// Starting the webserver
fmt.Println("WEB.SERVER-LOG: Starting WebServer...")
// Setting the certificate and key for SSL, that we generated with CERTGEN or other
certFile := "assets/certificates/certificate.pem" certFile := "assets/certificates/certificate.pem"
keyFile := "assets/certificates/private.key" keyFile := "assets/certificates/private.key"
// Launching go routines for the servers, not sure if this is how you correctly do this.
// Same goes for DNS where I used a similar approach...
go startHTTPSServer(certFile, keyFile) go startHTTPSServer(certFile, keyFile)
fmt.Println("WEB.SERVER-LOG: WebServer HTTPS is ready and listening on *:443") fmt.Println("WEB.SERVER-LOG: WebServer HTTPS is ready and listening on *" + config.HttpsPort)
go startHTTPServer(config.HttpPort) go startHTTPServer(config.HttpPort)
fmt.Println("WEB.SERVER-LOG: WebServer HTTP is ready and listening on *:80") fmt.Println("WEB.SERVER-LOG: WebServer HTTP is ready and listening on *" + config.HttpPort)
} }
func startHTTPSServer(certFile, keyFile string) { func startHTTPSServer(certFile, keyFile string) {
// Setting up app folder as the "main" root for serving (static)/JS files
fileServer := http.FileServer(http.Dir("app")) fileServer := http.FileServer(http.Dir("app"))
// Serving the main app at /
http.Handle("/", logHandler(fileServer)) http.Handle("/", logHandler(fileServer))
// Setting up location for the certificate for ATV, without exposing the private key.
http.HandleFunc("/certificate.cer", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/certificate.cer", func(w http.ResponseWriter, r *http.Request) {
logRequest(r) logRequest(r)
http.ServeFile(w, r, "assets/certificates/certificate.cer") http.ServeFile(w, r, "assets/certificates/certificate.cer")
@@ -57,7 +47,6 @@ func startHTTPSServer(certFile, keyFile string) {
http.HandleFunc("/log", jclogHandler) http.HandleFunc("/log", jclogHandler)
// Set up the route for the atvcsettings info
settings := ATVCSettings{ settings := ATVCSettings{
Hello: "you have reached JCATHOST and i have got some goodies for u", Hello: "you have reached JCATHOST and i have got some goodies for u",
SysSoft: JellyCAT.Name, SysSoft: JellyCAT.Name,
@@ -71,38 +60,23 @@ func startHTTPSServer(certFile, keyFile string) {
handleATVCSettings(w, r, settings) handleATVCSettings(w, r, settings)
}) })
// And finally serving over tls
err := http.ListenAndServeTLS(config.HttpsPort, certFile, keyFile, nil) err := http.ListenAndServeTLS(config.HttpsPort, certFile, keyFile, nil)
if err != nil { if err != nil {
fmt.Print("\033[A\r") handleServerError("Error starting HTTPS server:", err)
fmt.Println("WEB.SERVER-ERR: Error starting HTTPS server:", err)
// Let's not check what the error is but just assume it's missing certificates
fmt.Println("WEB.SERVER-ERR: Did you run 'CERTGEN'? ")
// Don't exit, but give a chance to run certgen
resetCommand()
} }
} }
func startHTTPServer(addr string) { func startHTTPServer(addr string) {
// Setting up and running a simple HTTP server for certificate retrieving and other insecure tasks...
// Might have to set up that only specific locations can be accessed by HTTP and force the rest HTTPS #todo?
// There also has to be a better way instead of starting 2 server functions? help
err := http.ListenAndServe(addr, nil) err := http.ListenAndServe(addr, nil)
if err != nil { if err != nil {
// Just error don't close out handleServerError("Error starting HTTP server:", err)
fmt.Println("WEB.SERVER-ERR: Error starting HTTP server:", err)
} }
} }
func handleATVCSettings(w http.ResponseWriter, r *http.Request, settings ATVCSettings) { func handleATVCSettings(w http.ResponseWriter, r *http.Request, settings ATVCSettings) {
// Set the content type to JSON
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
// allowing anyone to use this json with cors
w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Origin", "*")
// Encode the struct to JSON and write it to the response writer
err := json.NewEncoder(w).Encode(settings) err := json.NewEncoder(w).Encode(settings)
if err != nil { if err != nil {
logRequest(r) logRequest(r)
@@ -115,19 +89,18 @@ func jclogHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type") w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
return return
} }
// Read the request body
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
http.Error(w, "Error reading request body", http.StatusInternalServerError) http.Error(w, "Error reading request body", http.StatusInternalServerError)
return return
} }
// Parse the JSON payload
var logPayload JcLogEntry var logPayload JcLogEntry
err = json.Unmarshal(body, &logPayload) err = json.Unmarshal(body, &logPayload)
if err != nil { if err != nil {
@@ -135,27 +108,30 @@ func jclogHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
// Handle the log data as needed
fmt.Print("\033[A\r") fmt.Print("\033[A\r")
fmt.Printf("JellyCAT-APP-LOG: Received log at %s:\nJellyCAT-APP-LOG:\t%s\n", logPayload.Timestamp, logPayload.LogData) fmt.Printf("JellyCAT-APP-LOG: Received log at %s:\nJellyCAT-APP-LOG:\t%s\n", logPayload.Timestamp, logPayload.LogData)
resetCommand() resetCommand()
// Respond to the client
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
func logHandler(next http.Handler) http.Handler { func logHandler(next http.Handler) http.Handler {
// Basic logging function to see all incoming traffic, as im not sure yet what the ATV exactly wants
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logRequest(r) logRequest(r)
w.Header().Set("Access-Control-Allow-Origin", "*")
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })
} }
func logRequest(r *http.Request) { func logRequest(r *http.Request) {
// Write to the console with my hacky print & reset :)
fmt.Print("\033[A\r") fmt.Print("\033[A\r")
fmt.Printf("WEB.SERVER-LOG: [%s] %s %s \n", r.RemoteAddr, r.Method, r.URL) fmt.Printf("WEB.SERVER-LOG: [%s] %s %s \n", r.RemoteAddr, r.Method, r.URL)
// fmt.Println(r.Body, r.Header) resetCommand()
}
func handleServerError(message string, err error) {
fmt.Print("\033[A\r")
fmt.Println("WEB.SERVER-ERR: ", message, err)
fmt.Println("WEB.SERVER-ERR: Did you run 'CERTGEN'?")
resetCommand() resetCommand()
} }