From 1be97536efadb187e8be868589a93b41c495f98c Mon Sep 17 00:00:00 2001 From: Sepp Jeremiah Morris Date: Mon, 5 Feb 2024 00:23:38 +0100 Subject: [PATCH] Done for tonight, project is becoming to big for "ctrl+z version control"... Start using GIT --- .gitignore | 7 + app.go | 306 ++++++++++++++++++++++++ app/application.js | 582 +++++++++++++++++++++++++++++++++++++++++++++ app/bag.plist | 41 ++++ app/js/jclog.js | 56 +++++ app/xml/home.xml | 33 +++ configLoader.go | 40 ++++ dnsResolver.go | 66 +++++ go.mod | 15 ++ go.sum | 14 ++ main.go | 54 +++++ readme.md | 1 + settings.cfg | 31 +++ webServer.go | 161 +++++++++++++ 14 files changed, 1407 insertions(+) create mode 100644 .gitignore create mode 100644 app.go create mode 100644 app/application.js create mode 100644 app/bag.plist create mode 100644 app/js/jclog.js create mode 100644 app/xml/home.xml create mode 100644 configLoader.go create mode 100644 dnsResolver.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 readme.md create mode 100644 settings.cfg create mode 100644 webServer.go 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 @@ + + + +