commit 1be97536efadb187e8be868589a93b41c495f98c Author: Sepp Jeremiah Morris Date: Mon Feb 5 00:23:38 2024 +0100 Done for tonight, project is becoming to big for "ctrl+z version control"... Start using GIT diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9cbf32f --- /dev/null +++ b/.gitignore @@ -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 diff --git a/app.go b/app.go new file mode 100644 index 0000000..74397a9 --- /dev/null +++ b/app.go @@ -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 +} diff --git a/app/application.js b/app/application.js new file mode 100644 index 0000000..58e1077 --- /dev/null +++ b/app/application.js @@ -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 = ' \ + \ + \ + \ + <![CDATA[' + message + ']]> \ + \ + \ + \ + '; + + 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, '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, '>' ); + + return string; +}; + +console.log('EOF!'); + diff --git a/app/bag.plist b/app/bag.plist new file mode 100644 index 0000000..a0f6e6f --- /dev/null +++ b/app/bag.plist @@ -0,0 +1,41 @@ + + + + + auth-type + js + enabled + YES + menu-title + JellyCAT + merchant + com.jellycat.appletv + sandbox-mode + NO + menu-icon-url + + 720 + http://jcathost.dns/assets/img/icons/jcat@720.png + 1080 + http://jcathost.dns/assets/img/icons/jcat@1080.png + + menu-icon-url-version + 7 + app-dictionary + + adam-id + 700636371 + app-version + 732333 + bundle-identifier + com.jellycat.appletv + bundle-version + 1.0 + + + javascript-url + ./js/application.js + + + + \ No newline at end of file diff --git a/app/js/jclog.js b/app/js/jclog.js new file mode 100644 index 0000000..c924a9e --- /dev/null +++ b/app/js/jclog.js @@ -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)); +} \ No newline at end of file diff --git a/app/xml/home.xml b/app/xml/home.xml new file mode 100644 index 0000000..010a215 --- /dev/null +++ b/app/xml/home.xml @@ -0,0 +1,33 @@ + + + +