mirror of
https://github.com/SEPPDROID/JellyCAT.git
synced 2025-10-22 07:54:27 +00:00
Done for tonight, project is becoming to big for "ctrl+z version control"... Start using GIT
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal 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
306
app.go
Normal 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
582
app/application.js
Normal 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( /&/g, 'and' )
|
||||
.replace( /&/g, 'and' )
|
||||
.replace( /</g, 'less than' )
|
||||
.replace( /\</g, 'less than' )
|
||||
.replace( />/g, 'greater than' )
|
||||
.replace( /\>/g, 'greater than' );
|
||||
|
||||
return string;
|
||||
}
|
||||
};
|
||||
|
||||
// Extend atv.ProxyDocument to load errors from a message and description.
|
||||
if( atv.ProxyDocument ) {
|
||||
atv.ProxyDocument.prototype.loadError = function(message, description) {
|
||||
var doc = atvutils.makeErrorDocument(message, description);
|
||||
this.loadXML(doc);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// atv.Document extensions
|
||||
if( atv.Document ) {
|
||||
atv.Document.prototype.getElementById = function(id) {
|
||||
var elements = this.evaluateXPath("//*[@id='" + id + "']", this);
|
||||
if ( elements && elements.length > 0 ) {
|
||||
return elements[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// atv.Element extensions
|
||||
if( atv.Element ) {
|
||||
atv.Element.prototype.getElementsByTagName = function(tagName) {
|
||||
return this.ownerDocument.evaluateXPath("descendant::" + tagName, this);
|
||||
}
|
||||
|
||||
atv.Element.prototype.getElementByTagName = function(tagName) {
|
||||
var elements = this.getElementsByTagName(tagName);
|
||||
if ( elements && elements.length > 0 ) {
|
||||
return elements[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Simple Array Sorting methods
|
||||
Array.prototype.sortAsc = function() {
|
||||
this.sort(function( a, b ){
|
||||
return a - b;
|
||||
});
|
||||
};
|
||||
|
||||
Array.prototype.sortDesc = function() {
|
||||
this.sort(function( a, b ){
|
||||
return b - a;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Date methods and properties
|
||||
Date.lproj = {
|
||||
"DAYS": {
|
||||
"en": {
|
||||
"full": ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
|
||||
"abbrv": ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||
},
|
||||
"en_GB": {
|
||||
"full": ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
|
||||
"abbrv": ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||
}
|
||||
},
|
||||
"MONTHS": {
|
||||
"en": {
|
||||
"full": ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
||||
"abbrv": ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||
},
|
||||
"en_GB": {
|
||||
"full": ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
||||
"abbrv": ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Date.prototype.getLocaleMonthName = function( type ) {
|
||||
var language = atv.device.language,
|
||||
type = ( type === true ) ? "abbrv" : "full",
|
||||
MONTHS = Date.lproj.MONTHS[ language ] || Date.lproj.MONTHS[ "en" ];
|
||||
|
||||
return MONTHS[ type ][ this.getMonth() ];
|
||||
};
|
||||
|
||||
Date.prototype.getLocaleDayName = function( type ) {
|
||||
var language = atv.device.language,
|
||||
type = ( type === true ) ? "abbrv" : "full",
|
||||
DAYS = Date.lproj.DAYS[ language ] || Date.lproj.DAYS[ "en" ];
|
||||
|
||||
return DAYS[ type ][ this.getDay() ];
|
||||
};
|
||||
|
||||
Date.prototype.nextDay = function( days ) {
|
||||
var oneDay = 86400000,
|
||||
days = days || 1;
|
||||
this.setTime( new Date( this.valueOf() + ( oneDay * days ) ) );
|
||||
};
|
||||
|
||||
Date.prototype.prevDay = function( days ) {
|
||||
var oneDay = 86400000,
|
||||
days = days || 1;
|
||||
this.setTime( new Date( this.valueOf() - ( oneDay * days ) ) );
|
||||
};
|
||||
|
||||
|
||||
// String Trim methods
|
||||
String.prototype.trim = function ( ch )
|
||||
{
|
||||
var ch = ch || '\\s',
|
||||
s = new RegExp( '^['+ch+']+|['+ch+']+$','g');
|
||||
return this.replace(s,'');
|
||||
};
|
||||
|
||||
String.prototype.trimLeft = function ( ch )
|
||||
{
|
||||
var ch = ch || '\\s',
|
||||
s = new RegExp( '^['+ch+']+','g');
|
||||
return this.replace(s,'');
|
||||
};
|
||||
|
||||
String.prototype.trimRight = function ( ch )
|
||||
{
|
||||
var ch = ch || '\\s',
|
||||
s = new RegExp( '['+ch+']+$','g');
|
||||
return this.replace(s,'');
|
||||
};
|
||||
|
||||
String.prototype.xmlEncode = function()
|
||||
{
|
||||
var string = unescape( this );
|
||||
|
||||
string = string
|
||||
.replace( /&/g, '&' )
|
||||
.replace( /\</g, '<' )
|
||||
.replace( /\>/g, '>' );
|
||||
|
||||
return string;
|
||||
};
|
||||
|
||||
console.log('EOF!');
|
||||
|
41
app/bag.plist
Normal file
41
app/bag.plist
Normal 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
56
app/js/jclog.js
Normal 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
33
app/xml/home.xml
Normal 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
40
configLoader.go
Normal 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
66
dnsResolver.go
Normal 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
15
go.mod
Normal 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
14
go.sum
Normal 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
54
main.go
Normal 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 {}
|
||||
}
|
31
settings.cfg
Normal file
31
settings.cfg
Normal 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
161
webServer.go
Normal 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()
|
||||
}
|
Reference in New Issue
Block a user