Done for tonight, project is becoming to big for "ctrl+z version control"... Start using GIT

This commit is contained in:
2024-02-05 00:23:38 +01:00
commit 1be97536ef
14 changed files with 1407 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
/.idea
/JellyCAT.exe
/assets/certificates/certificate.cer
/assets/certificates/certificate.pem
/assets/certificates/private.key
/app/assets/img/icons/jcat@720.png
/app/assets/img/icons/jcat@1080.png

306
app.go Normal file
View File

@@ -0,0 +1,306 @@
// /\_/|
// { ' ' } JellyCAT
// \____\
package main
import (
"bufio"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"os"
"os/exec"
"runtime"
"strings"
"time"
)
func app() {
// This is the main app function for all the logic I need
// jclogHelper is a function that im not sure is safe enough to keep... It listens for log information I send from the JS-App.
// But opens yet another hole...
fmt.Println("JellyCAT-LOG: Starting JellyCAT-LogHelper")
// Good news: Using the webserver now, so I won't have to add yet another go routine
// keeping the logMSG for aesthetics only :)
// go jclogHelper()
fmt.Println("JellyCAT-APP-LOG: Ready to print JellyCAT APP-LOG, listening on /log")
fmt.Println("JellyCAT-LOG: Starting CI...")
fmt.Println()
// Command interface, that makes it easy to add functionality, run or stop them (that was the plan)
for {
fmt.Print("Enter a command: ")
input := getUserInput()
switch strings.ToLower(input) {
case "help":
displayHelp()
case "exit":
fmt.Println("Exiting JellyCAT & Stopping Servers. Goodbye!")
os.Exit(0)
case "clear":
clearScreen()
case "cinfo":
displayCinfo()
case "certgen":
certGen()
default:
fmt.Println("Invalid command. Type 'help' for assistance, or 'exit' to quit")
fmt.Println()
}
}
}
func resetCommand() {
// A simple & hacky way of removing and resetting the "Enter command" prompt, works for now? but I need better logging... #todo
// I could've created a simple function that takes the log string and then resets the enter command text. Oh well.
fmt.Print("\033[K")
fmt.Println()
fmt.Print("Enter a command: ")
}
func getUserInput() string {
// Handles user input for the for case loop
reader := bufio.NewReader(os.Stdin)
input, _ := reader.ReadString('\n')
return strings.TrimSpace(input)
}
func displayHelp() {
// Simple help information print
fmt.Println()
fmt.Println("========== JellyCAT Help Menu ===========")
fmt.Println("| Available commands: |")
fmt.Println("=========================================")
fmt.Println("| - help : Display this help menu |")
fmt.Println("| - exit : Exit the application |")
fmt.Println("| - clear : Clear the screen |")
fmt.Println("| - cinfo : Config Info |")
fmt.Println("| - certgen : generate Cert&key |")
fmt.Println("=========================================")
fmt.Println()
fmt.Println()
}
func displayCinfo() {
// Current config information, also so I can see what the actual value is
fmt.Println()
fmt.Println("=========== JellyCAT Config =============")
fmt.Println("| Current config & Information: |")
fmt.Println("=========================================")
fmt.Println("| *** Services *** |")
fmt.Println("| |")
fmt.Println("| DNS SERVER : Enabled |") // nice static print, nerd hehehe
fmt.Println("| WEB SERVER : Enabled |")
fmt.Println("| |")
fmt.Println("| *** Current Config *** |")
fmt.Println("| |")
fmt.Println("| *** DNS *** |")
fmt.Println("| |")
fmt.Println("| hijack_ip = ", config.HijackIP, " |")
fmt.Println("| hijack_app = ", config.HijackApp, " |")
fmt.Println("| hijack_img = ", config.HijackImg, " |")
fmt.Println("| forward_ip = ", config.ForwardIP, " |") // any big ip address and my design tabs too far :( haha
fmt.Println("| |")
fmt.Println("| *** WEB *** |")
fmt.Println("| |")
fmt.Println("| https_port = ", config.HttpsPort, " |")
fmt.Println("| https_port = ", config.HttpPort, " |")
fmt.Println("| |")
fmt.Println("=========================================")
fmt.Println("| go to brain.seppjm.com/# |")
fmt.Println("| for more information & example cfg |")
fmt.Println("=========================================")
fmt.Println()
fmt.Println()
}
func clearScreen() {
// Clearing the screen, a bit spooky using runtime.GOOS and exec... But seems to be how others do it too?
switch runtime.GOOS {
case "linux", "darwin":
cmd := exec.Command("clear")
cmd.Stdout = os.Stdout
cmd.Run()
fmt.Println()
case "windows":
cmd := exec.Command("cmd", "/c", "cls")
cmd.Stdout = os.Stdout
cmd.Run()
fmt.Println()
default:
fmt.Println("JellyCAT-ERR: \t\tCan't clear screen: Unsupported operating system!")
// Better safe than sorry
}
}
func certGen() {
// Certgen is the function that generates a certificate and key set
fmt.Println()
fmt.Println("CERTGEN-LOG: \t\tGenerating key & certificates...")
// Generate a private key
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
fmt.Println("CERTGEN-ERR: \t\tError generating private key:", err)
return
}
// Generate a serial number for the certificate
// Doesn't seem completely necessary but openssl generates one, and so do we.
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
fmt.Println("CERTGEN-ERR: \t\tError generating certificate serial number:", err)
return
}
// Create a template for the certificate
// ATV is very picky, has to be like this or it no workie. Its working now, so I'm scared to touch/change anything
// ATV needs a common name not an organisation, my bad.
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Country: []string{"US"},
// Organization: []string{"appletv.flickr.com"},
CommonName: config.CertName,
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour * 20),
// KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
// ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IsCA: true,
}
// Add Subject Key Identifier extension, because openssl adds one.
subjectKeyID, err := generateSubjectKeyID(&privateKey.PublicKey)
if err != nil {
fmt.Println("CERTGEN-ERR: \t\tError generating Subject Key Identifier:", err)
return
}
template.SubjectKeyId = subjectKeyID
// Add Authority Key Identifier extension, because openssl adds one.
authorityKeyID, err := generateAuthorityKeyID(&privateKey.PublicKey)
if err != nil {
fmt.Println("CERTGEN-ERR: \t\tError generating Authority Key Identifier:", err)
return
}
template.AuthorityKeyId = authorityKeyID
// Generate the certificate
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
if err != nil {
fmt.Println("CERTGEN-ERR: \t\tError creating certificate:", err)
return
}
// Save private key to file
privateKeyFile, err := os.Create("assets/certificates/private.key")
if err != nil {
fmt.Println("CERTGEN-ERR: \t\tError creating private key file:", err)
return
}
err = pem.Encode(privateKeyFile, &pem.Block{Type: "PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
if err != nil {
fmt.Println("CERTGEN-ERR: \t\tError creating private key file:", err)
return
}
privateKeyFile.Close()
// Save certificate to file
certFile, err := os.Create("assets/certificates/certificate.pem")
if err != nil {
fmt.Println("CERTGEN-ERR: \t\tError creating certificate file:", err)
return
}
err = pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certDER})
if err != nil {
fmt.Println("CERTGEN-ERR: \t\tError creating certificate file:", err)
return
}
certFile.Close()
// Now we create a .cer security certificate from the PEM file for the ATV's profile system
fmt.Println("CERTGEN-LOG: \t\tCertificate and private key generated successfully.")
fmt.Println("CERTGEN-LOG: \t\tConverting PEM to DER for ATV Profile...")
generateDerCer()
}
func generateSubjectKeyID(publicKey *rsa.PublicKey) ([]byte, error) {
// Marshal the public key to DER format
publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey)
if err != nil {
return nil, err
}
// Calculate the SHA-1 hash of the DER-encoded public key
hash := sha1.Sum(publicKeyBytes)
return hash[:], nil
}
func generateAuthorityKeyID(publicKey *rsa.PublicKey) ([]byte, error) {
// Yup you guessed it, the openssl conf creates one too. (not like this I think lol)
return generateSubjectKeyID(publicKey)
}
func generateDerCer() {
inputFile := "assets/certificates/certificate.pem"
outputFile := "assets/certificates/certificate.cer"
err := convertPEMToDER(inputFile, outputFile)
if err != nil {
// stop goroutine on error
panic(err)
}
fmt.Println("CERTGEN-LOG: \t\tConversion to DER successful!")
fmt.Println("CERTGEN-LOG: \t\tPlease restart JellyCAT to load new certificate") // we could reload the webserver with the new cert, but nah.
fmt.Println()
}
func convertPEMToDER(inputFile string, outputFile string) error {
// Read the PEM-encoded certificate from the input file
pemData, err := os.ReadFile(inputFile)
if err != nil {
return err
}
// Decode the PEM block
block, _ := pem.Decode(pemData)
if block == nil {
return err
}
// Parse the X.509 certificate
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return err
}
// Convert the certificate to DER format
derCertificate := cert.Raw
// Create or open the DER-encoded certificate file for writing
outputFileHandle, err := os.Create(outputFile)
if err != nil {
return err
}
defer outputFileHandle.Close()
// Write the DER-encoded certificate to the output file
_, err = outputFileHandle.Write(derCertificate)
if err != nil {
return err
}
return nil
}

582
app/application.js Normal file
View File

@@ -0,0 +1,582 @@
// /\_/|
// { ' ' } JellyCAT
// \____\
atv.config = {
doesJavaScriptLoadRoot: true
};
atv.onAppEntry = function () {
fetchSettings(function() {
overrideConsoleLog();
console.log("Successfully overwritten console log, sending logs to JCATHOST now.")
console.log("Received ATVCSETTINGS From JCATHOST.");
console.log("Starting JellyCAT-JS on AppleTV...")
jcatMain();
});
}
atv.onAppExit = function() {
console.log('Exiting App!');
};
// ***************************************************
// JellyCAT Main | Main JS app function
// Help
function jcatMain(){
atvutils.loadURL("https://" + atv.jcathost.SigHost + "/xml/home.xml");
}
// ***************************************************
// 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 = "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
// Function to 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('EOF!');

41
app/bag.plist Normal file
View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>auth-type</key>
<string>js</string>
<key>enabled</key>
<string>YES</string>
<key>menu-title</key>
<string>JellyCAT</string>
<key>merchant</key>
<string>com.jellycat.appletv</string>
<key>sandbox-mode</key>
<string>NO</string>
<key>menu-icon-url</key>
<dict>
<key>720</key>
<string>http://jcathost.dns/assets/img/icons/jcat@720.png</string>
<key>1080</key>
<string>http://jcathost.dns/assets/img/icons/jcat@1080.png</string>
</dict>
<key>menu-icon-url-version</key>
<string>7</string>
<key>app-dictionary</key>
<dict>
<key>adam-id</key>
<integer>700636371</integer>
<key>app-version</key>
<integer>732333</integer>
<key>bundle-identifier</key>
<string>com.jellycat.appletv</string>
<key>bundle-version</key>
<string>1.0</string>
</dict>
<key>javascript-url</key>
<string>./js/application.js</string>
</dict>
</plist>

56
app/js/jclog.js Normal file
View File

@@ -0,0 +1,56 @@
// /\_/|
// { ' ' } 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));
}

33
app/xml/home.xml Normal file
View File

@@ -0,0 +1,33 @@
<?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>

40
configLoader.go Normal file
View File

@@ -0,0 +1,40 @@
// /\_/|
// { ' ' } JellyCAT
// \____\
package main
import (
"fmt"
"github.com/BurntSushi/toml"
"os"
)
type Config struct {
HijackIP string `toml:"hijack_ip"`
HijackApp string `toml:"hijack_app"`
HijackImg string `toml:"hijack_img"`
ForwardIP string `toml:"forward_ip"`
HttpsPort string `toml:"https_port"`
HttpPort string `toml:"http_port"`
CertName string `toml:"common_name"`
}
var config Config
func loadConfig() {
// Reading config from settings.cfg file
fmt.Println("SYS-LOG: Loading Config...")
data, err := os.ReadFile("settings.cfg")
if err != nil {
fmt.Println("SYS-ERR: Error reading 'settings.cfg' config file:", err)
os.Exit(1)
}
if _, err := toml.Decode(string(data), &config); err != nil {
fmt.Println("SYS-ERR: Error decoding config file:", err)
os.Exit(1)
}
fmt.Println("SYS-LOG: Config Loaded!")
// Config loaded and ready to go back to the main function!
}

66
dnsResolver.go Normal file
View File

@@ -0,0 +1,66 @@
// /\_/|
// { ' ' } JellyCAT
// \____\
package main
import (
"fmt"
"net"
"os"
"github.com/miekg/dns"
)
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"}
dns.HandleFunc(".", handleDNSRequest)
go func() {
if err := server.ListenAndServe(); err != nil {
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)
}
}()
fmt.Println("DNS.SERVER-LOG: DNS server is ready and listening on *:53")
}
func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
m := new(dns.Msg)
m.SetReply(r)
m.Compress = false
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) {
// Resolve hijack domain app to the IP addresses from the config
// Had to add some trippy quadA detection, since ipv6 (AAAA) requests would end up funky
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 {
// Forward other requests to the IP address from the config
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)
if err != nil {
return
}
}

15
go.mod Normal file
View File

@@ -0,0 +1,15 @@
module JellyCAT
go 1.22rc2
require (
github.com/BurntSushi/toml v1.3.2
github.com/miekg/dns v1.1.58
)
require (
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/tools v0.17.0 // indirect
)

14
go.sum Normal file
View File

@@ -0,0 +1,14 @@
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=

54
main.go Normal file
View File

@@ -0,0 +1,54 @@
// /\_/|
// { ' ' } JellyCAT
// \____\
package main
import (
"fmt"
)
type JcatDefaults struct {
// For Setting defaults
Version string
Name string
HostName string
HostIP string
}
var JellyCAT JcatDefaults
func main() {
// Load config file for config struct
fmt.Println()
fmt.Println("SYS-LOG: Attempting to load the config...")
loadConfig()
// Default information store
JellyCAT = JcatDefaults{
Version: "0.1.1revB",
Name: "JellyCAT Serving stHack",
HostName: config.CertName,
HostIP: config.HijackIP,
}
// Starting main JellyCAT function
fmt.Println()
fmt.Println(" JellyCAT", JellyCAT.Version)
fmt.Println()
// DNS Server & Resolver function for hijacking and forwarding DNS requests
fmt.Println("SYS-LOG: Attempting to start DNS Server...")
dnsResolver()
// Webserver for serving x and app to the ATV
fmt.Println("SYS-LOG: Attempting to start WEB Server...")
webServer()
// App main for any other server-sided logic
fmt.Println("SYS-LOG: Attempting to start JellyCAT-Main...")
app()
// This is working to keep all the functions alive, But is it the correct way? lmk if it's incorrect
select {}
}

1
readme.md Normal file
View File

@@ -0,0 +1 @@
w i p

31
settings.cfg Normal file
View File

@@ -0,0 +1,31 @@
# /\_/|
# { ' ' } JellyCAT
# \____\
# Edit your custom settings here
# DNS Settings
# hijack_ip = the ip address that the dns server sends out as the A record, make this your JellyCAT Host
# hijack_app = the main domain we want to use to hijack, we use this domain to intercept DNS
# hijack_img = the domain we intercept for the imaging (branding) of the app we want to hijack (this function is currently removed)
# forward_ip = the ip address of an dns server we want to forward uninteresting DNS requests to
hijack_ip = "192.168.11.23"
hijack_app = "atv.qello.com."
hijack_img = "notused.tld."
forward_ip = "1.1.1.1"
# WEBServer Settings
# https_port = the port you want to open for the https webserver with the self signed certificate
# http_port = the port you want to open for the http webserver (edit for use with reverse proxy)
https_port = ":443"
http_port = ":80"
# CERTGEN Settings
# common_name = hostname for generating certificate
common_name = "atv.qello.com"

161
webServer.go Normal file
View File

@@ -0,0 +1,161 @@
// /\_/|
// { ' ' } JellyCAT
// \____\
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
type ATVCSettings struct {
Hello string `json:"hello"`
SysSoft string `json:"system"`
Version string `json:"version"`
SigHost string `json:"sig_host"`
SigHostP string `json:"sig_host_p"`
HostIP string `json:"host_ip"`
}
type JcLogEntry struct {
Timestamp string `json:"timestamp"`
LogData string `json:"logData"`
}
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"
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)
fmt.Println("WEB.SERVER-LOG: WebServer HTTPS is ready and listening on *:443")
go startHTTPServer(config.HttpPort)
fmt.Println("WEB.SERVER-LOG: WebServer HTTP is ready and listening on *:80")
}
func startHTTPSServer(certFile, keyFile string) {
// Setting up app folder as the "main" root for serving (static)/JS files
fileServer := http.FileServer(http.Dir("app"))
// Serving the main app at /
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) {
logRequest(r)
http.ServeFile(w, r, "assets/certificates/certificate.cer")
})
http.HandleFunc("/log", jclogHandler)
// Set up the route for the atvcsettings info
settings := ATVCSettings{
Hello: "you have reached JCATHOST and i have got some goodies for u",
SysSoft: JellyCAT.Name,
Version: JellyCAT.Version,
SigHost: JellyCAT.HostName,
SigHostP: config.HttpsPort,
HostIP: config.HijackIP,
}
http.HandleFunc("/atvcsettings", func(w http.ResponseWriter, r *http.Request) {
logRequest(r)
handleATVCSettings(w, r, settings)
})
// And finally serving over tls
err := http.ListenAndServeTLS(config.HttpsPort, certFile, keyFile, nil)
if err != nil {
fmt.Print("\033[A\r")
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) {
// 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)
if err != nil {
// Just error don't close out
fmt.Println("WEB.SERVER-ERR: Error starting HTTP server:", err)
}
}
func handleATVCSettings(w http.ResponseWriter, r *http.Request, settings ATVCSettings) {
// Set the content type to JSON
w.Header().Set("Content-Type", "application/json")
// allowing anyone to use this json with cors
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)
if err != nil {
logRequest(r)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func jclogHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method != http.MethodPost {
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
return
}
// Read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
// Parse the JSON payload
var logPayload JcLogEntry
err = json.Unmarshal(body, &logPayload)
if err != nil {
http.Error(w, "Error parsing JSON payload", http.StatusBadRequest)
return
}
// Handle the log data as needed
fmt.Print("\033[A\r")
fmt.Printf("JellyCAT-APP-LOG: Received log at %s:\nJellyCAT-APP-LOG:\t%s\n", logPayload.Timestamp, logPayload.LogData)
resetCommand()
// Respond to the client
w.WriteHeader(http.StatusOK)
}
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) {
logRequest(r)
next.ServeHTTP(w, r)
})
}
func logRequest(r *http.Request) {
// Write to the console with my hacky print & reset :)
fmt.Print("\033[A\r")
fmt.Printf("WEB.SERVER-LOG: [%s] %s %s \n", r.RemoteAddr, r.Method, r.URL)
// fmt.Println(r.Body, r.Header)
resetCommand()
}