/* Copyright (C) 2025 etaHEN / LightningMods
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 3, or (at your option) any
later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; see the file COPYING. If not, see
. */
#include "ipc.hpp"
#include
#include
#include
#include
extern "C" {
#include "common_utils.h"
#include
#include
#include
int sceKernelMprotect(void *addr, size_t len, int prot);
pid_t elfldr_spawn(const char* cwd, int stdio, uint8_t* elf, const char* name);
extern uint8_t elfldr_start[];
extern const unsigned int elfldr_size;
int sceSystemServiceLoadExec(const char *path, const char *arg);
extern bool is_handler_enabled;
}
#include "../extern/tiny-json/tiny-json.hpp"
#include
#include
#include
#include
#include
#include
#include
extern pthread_t cmd_server;
void* runCommandNControlServer(void*);
// pop -Winfinite-recursion error for this func for clang
#define MB(x) ((size_t)(x) << 20)
#define READ_SIZE 0x1024
extern "C" void shutdown_klog(void);
extern atomic_bool no_network_rest_mode_action, real_rest_mode_detected;
extern int shellui_pid_for_comp;
extern uintptr_t code_addr;
extern char ip_address[];
int DaemonSocket = 0;
bool startDirectPKGInstaller(bool is_v2);
bool if_exists(const char *path);
extern "C" int launchApp(const char *titleId);
bool if_exists(const char *path);
void activate_shellui_patch();
bool LoadSettings();
bool rmtree(const char *path) {
DIR *dir = opendir(path);
if (dir == NULL) {
etaHEN_log("Error opening directory %s", path);
return false;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
// Skip "." and ".." entries
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
char path_1[1000];
snprintf(path_1, sizeof(path_1), "%s/%s", path, entry->d_name);
if (entry->d_type == DT_DIR) {
// Recursive call for subdirectories
rmtree(path_1);
} else {
// Delete files
if (unlink(path_1) != 0) {
// perror("Error deleting file");
etaHEN_log("Error deleting file %s", path);
}
}
}
closedir(dir);
// Delete the empty folder
if (rmdir(path) != 0) {
// perror("Error deleting folder");
etaHEN_log("Error deleting folder %s", path);
}
return true;
}
struct sockaddr_in networkAdress(uint16_t port) {
struct sockaddr_in address;
address.sin_len = sizeof(address);
address.sin_family = AF_INET;
address.sin_port = htons(port);
memset(address.sin_zero, 0, sizeof(address.sin_zero));
return address;
}
int networkListen(const char *soc_path) {
struct sockaddr_un server;
unlink(soc_path);
etaHEN_log("[Daemon] Deleted Socket...");
int s = socket(AF_UNIX, SOCK_STREAM, 0);
if (s < 0) {
etaHEN_log("[Daemon] Socket failed! %s", strerror(errno));
return INVAIL;
}
memset(&server, 0, sizeof(server));
server.sun_family = AF_UNIX;
strcpy(server.sun_path, soc_path);
int r = bind(s, (struct sockaddr *)&server, SUN_LEN(&server));
if (r < 0) {
etaHEN_log("[Daemon] Bind failed! %s", strerror(errno));
return INVAIL;
}
// etaHEN_log("Socket has name %s", server.sun_path);
r = listen(s, 100);
if (r < 0) {
etaHEN_log("[Daemon] listen failed! %s", strerror(errno));
return INVAIL;
}
return s;
}
int networkAccept(int socket) {
return accept(socket, 0, 0);
}
int networkReceiveData(int socket, void *buffer, int32_t size) {
int nu = recv(socket, buffer, size, 0);
etaHEN_log("got %i bytes", nu);
return nu;
}
int networkSendData(int socket, void *buffer, int32_t size) {
return send(socket, buffer, size, MSG_NOSIGNAL);
}
int networkSendDebugData(void *buffer, int32_t size) {
return networkSendData(DaemonSocket, buffer, size);
}
int networkCloseConnection(int socket) { return close(socket); }
int networkCloseDebugConnection() {
return networkCloseConnection(DaemonSocket);
}
void reply(int sender_socket, bool error, std::string out_var = "Nothing") {
std::string inputStr = "{\"res\":" + std::to_string(error ? -1 : 0) +
", \"var\":\"" + out_var + "\"}";
IPCMessage outputMessage;
outputMessage.cmd = BREW_UTIL_RETURN_VALUE;
outputMessage.error = error ? -1 : 0;
etaHEN_log("error: %d", outputMessage.error);
if (!inputStr.empty()) {
strncpy(outputMessage.msg, inputStr.c_str(), sizeof(outputMessage.msg) - 1);
// Null-terminate the destination array
outputMessage.msg[sizeof(outputMessage.msg) - 1] = '\0';
}
networkSendData(sender_socket, reinterpret_cast(&outputMessage),
sizeof(outputMessage));
}
std::vector readFile(std::string filename) {
// open the file:
std::ifstream file(filename, std::ios::binary);
if (!file.is_open()) {
etaHEN_log("Failed to open %s", filename.c_str());
return std::vector();
}
// Stop eating new lines in binary mode!!!
file.unsetf(std::ios::skipws);
// get its size:
std::streampos fileSize;
file.seekg(0, std::ios::end);
fileSize = file.tellg();
file.seekg(0, std::ios::beg);
// reserve capacity
std::vector vec;
vec.reserve(fileSize);
// read the data:
vec.insert(vec.begin(), std::istream_iterator(file),
std::istream_iterator());
return vec;
}
std::string GetPS5Version(const std::string &jsonpath) {
try {
std::ifstream input_file(jsonpath);
if (!input_file.is_open()) {
etaHEN_log("Failed to open file for reading: %s", jsonpath.c_str());
return "Error Opening Json";
}
nlohmann::json j;
input_file >> j;
input_file.close();
if (j.contains("contentVersion"))
return std::string(j["contentVersion"]);
} catch (const std::exception &e) {
// Handle exceptions here, you can log the error or perform other error
// handling tasks
etaHEN_log("An exception occurred: %s", e.what());
return "Error getting version";
}
return "Error getting version";
}
// Callback function to write received data
size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) {
FILE *fp = (FILE *)userp;
return fwrite(contents, size, nmemb, fp);
}
void handleIPC(struct clientArgs *client, std::string &inputStr,
DaemonCommands command) {
constexpr uint32_t MAX_TOKENS = 256;
json_t pool[MAX_TOKENS]{};
int sender_app = client->socket;
std::string path_buf, path_buf2, json_path;
char temp[0x255];
std::string out_var = "Nothing"; // default send var
etaHEN_log("Received IPC command 0x%X", command);
// etaHEN_log("Received IPC data: %s", inputStr.c_str());
json_t const *my_json =
inputStr.empty()
? NULL
: json_create((char *)inputStr.c_str(), pool, MAX_TOKENS);
if (!my_json) {
etaHEN_log("Error parsing JSON");
notify(true, "Error parsing JSON");
reply(sender_app, true);
return;
}
switch (command) {
case BREW_UTIL_SHELLUI_ON_STANDBY: {
etaHEN_log("ShellUI on standby");
real_rest_mode_detected = no_network_rest_mode_action = true;
reply(sender_app, false);
break;
}
case BREW_UTIL_TOGGLE_FTP: {
bool turn_on = (bool)json_getInteger(json_getProperty(my_json, "toggle"));
etaHEN_log("FTP toggle: %d", turn_on);
if (turn_on) {
if (StartFTP()) {
notify(true, "FTP Server Started\nIP: %s Port: 1337", ip_address);
reply(sender_app, false);
break;
} else
reply(sender_app, true);
} else {
ShutdownFTP();
notify(true, "FTP Server Stopped");
reply(sender_app, false);
}
break;
}
case BREW_UTIL_TOGGLE_KLOG: {
bool turn_on = (bool)json_getInteger(json_getProperty(my_json, "toggle"));
etaHEN_log("klog toggle: %d", turn_on);
if (turn_on) {
if (start_klog()) {
notify(true, "Klog Server Started\nIP: %s Port: 9081", ip_address);
reply(sender_app, false);
} else
reply(sender_app, true);
} else {
shutdown_klog();
notify(true, "Klog Server Stopped");
reply(sender_app, false);
}
break;
}
case BREW_UTIL_TOGGLE_DPI: {
bool turn_on = (bool)json_getInteger(json_getProperty(my_json, "toggle"));
bool is_v2 = (bool)json_getInteger(json_getProperty(my_json, "is_v2"));
etaHEN_log("DPI toggle: %d | is_v2 %s", turn_on, is_v2 ? "true" : "false");
if (turn_on) {
if (startDirectPKGInstaller(is_v2)) {
notify(true,
is_v2 ? "Direct PKG Installer V2 Server Started\nWebUI: "
"http://%s:12800 "
: "Direct PKG Installer Server Started\nIP: %s Port: 9090",
ip_address);
reply(sender_app, false);
} else
reply(sender_app, true);
} else {
shutdownDirectPKGInstaller(is_v2);
notify(true, is_v2 ? "Direct PKG Installer V2 Server Stopped"
: "Direct PKG Installer Server Stopped");
reply(sender_app, false);
}
break;
}
case BREW_UTIL_DAEMON_PID: {
snprintf(temp, sizeof(temp), "%d", getpid());
reply(sender_app, false, temp);
break;
}
case BREW_UTIL_GET_GAME_VER: {
auto tid = std::string(json_getPropertyValue(my_json, "tid"));
if (tid.empty()) {
notify(true, "Failed to get tid");
reply(sender_app, true);
break;
}
std::string tmp, game_version;
bool is_PS5 = tid.rfind("PPSA", 0) == 0; // Check if tid starts with "PPSA"
if (is_PS5) {
// Attempt to load JSON files for PS5 games
tmp = "/system_data/priv/appmeta/" + tid + "/param.json";
if (!if_exists(tmp.c_str())) {
etaHEN_log("%s: json %s does not exist", tid.c_str(), tmp.c_str());
tmp = "/system_data/priv/appmeta/external/" + tid + "/param.json";
if (!if_exists(tmp.c_str())) {
etaHEN_log("%s: json %s does not exist", tid.c_str(), tmp.c_str());
tmp = "/system_ex/app/" + tid + "/sce_sys/param.json";
if (!if_exists(tmp.c_str())) {
etaHEN_log("%s: json %s does not exist", tid.c_str(), tmp.c_str());
notify(true, "Failed to get game version");
reply(sender_app, true);
break;
}
}
}
game_version = GetPS5Version(tmp);
if (game_version.empty()) {
notify(true, "Failed to get game version");
etaHEN_log("Failed to get game version for PS5 Game");
reply(sender_app, true);
break;
}
} else {
// Attempt to load SFO files for PS4 games
tmp = "/system_data/priv/appmeta/" + tid + "/param.sfo";
if (!if_exists(tmp.c_str())) {
etaHEN_log("%s: sfo %s does not exist", tid.c_str(), tmp.c_str());
tmp = "/system_data/priv/appmeta/external/" + tid + "/param.sfo";
if (!if_exists(tmp.c_str())) {
etaHEN_log("%s: sfo %s does not exist", tid.c_str(), tmp.c_str());
notify(true, "Failed to get game version");
reply(sender_app, true);
break;
}
}
std::vector sfo_data = readFile(tmp);
if (sfo_data.empty()) {
notify(true, "Failed to read SFO file");
reply(sender_app, true);
break;
}
SfoReader sfo(sfo_data);
// VERSION key holds the original version, it doesn't change if updated
try {
std::string version_str = sfo.GetValueFor("VERSION");
std::string app_ver_str = sfo.GetValueFor("APP_VER");
float version_val = std::stof(version_str);
float app_ver_val = std::stof(app_ver_str);
game_version = (version_val > app_ver_val) ? version_str : app_ver_str;
}
catch (const std::exception& e) {
// Fallback to APP_VER if there's an issue
game_version = sfo.GetValueFor("APP_VER");
}
}
etaHEN_log("Version: %s", game_version.c_str());
reply(sender_app, false, game_version);
break;
}
case BREW_UTIL_LAUNCH_PLUGIN: {
std::string plugin_path =
std::string(json_getPropertyValue(my_json, "plugin_path"));
std::string title_id =
std::string(json_getPropertyValue(my_json, "title_id"));
etaHEN_log("Launching %s (TID: %s)", plugin_path.c_str(),
title_id.c_str());
if (!load_plugin(plugin_path.c_str())) {
notify(true, "Failed to Load in\nPath: %s\nTID: %s",
plugin_path.c_str(), title_id.c_str());
reply(sender_app, true);
break;
}
notify(true, "Plugin or ELF launched successfully\nPath: %s\nTID: %s",
plugin_path.c_str(), title_id.c_str());
reply(sender_app, false);
break;
}
case BREW_UTIL_GET_GAME_CHEAT: {
std::string title_id = std::string(json_getPropertyValue(my_json, "tid"));
std::string version =
std::string(json_getPropertyValue(my_json, "version"));
GameCheat *cheat = CheatManager::GetGameCheat(title_id, version);
if (cheat) {
//
// Build json response, we need escape the quotes because the IPC response
// is also between quotes, which break the JSON response
//
nlohmann::json res_json;
// Set the name
res_json["name"] = cheat->name;
// Build the cheats array
for (size_t i = 0; i < cheat->cheats.size(); ++i) {
nlohmann::json cheat_entry;
cheat_entry["name"] = cheat->cheats[i].name;
cheat_entry["id"] = static_cast(i);
cheat_entry["enabled"] = cheat->cheats[i].enabled;
cheat_entry["description"] = cheat->cheats[i].description;
res_json["cheats"].push_back(cheat_entry);
}
// Build the authors array
for (size_t i = 0; i < cheat->authors.size(); ++i) {
res_json["authors"].push_back(cheat->authors[i]);
}
std::string res = res_json.dump();
#if SHELL_DEBUG == 1
etaHEN_log("Response json => %s (%d bytes)", res.c_str(), res.size());
#endif
//
// Create a shared file contained the parsed cheat
//
std::string shm_path = "/user/data/etaHEN/" + title_id + "_cheats";
unlink(shm_path.c_str());
int fd = open(shm_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd >= 0) {
// Write the buffer to the file
if (write(fd, res.c_str(), res.length()) == -1) {
perror("write failed");
}
// Close the file descriptor
close(fd);
}
reply(sender_app, false, shm_path);
} else {
notify(true, "No cheats available for %s version %s!", title_id.c_str(),
version.c_str());
reply(sender_app, true);
}
break;
}
case BREW_UTIL_TOGGLE_CHEAT: {
std::string title_id = std::string(json_getPropertyValue(my_json, "tid"));
json_t const *cheat_id_property = json_getProperty(my_json, "cheat_id");
json_t const *target_pid_property = json_getProperty(my_json, "pid");
int pid = json_getInteger(target_pid_property);
int cheat_id = json_getInteger(cheat_id_property);
std::string cheat_name;
etaHEN_log("Received toggle command for cheat %d on %s PID %d ID %d",
cheat_id, title_id.c_str(), pid, cheat_id);
if (CheatManager::ToggleCheat(pid, title_id, cheat_id, cheat_name)) {
etaHEN_log("Cheat successfully activated!");
reply(sender_app, false, cheat_name);
} else {
reply(sender_app, true);
}
break;
}
case BREW_UTIL_LAUNCH_ELFLDR: {
#if 1
if (elfldr_spawn("/", STDOUT_FILENO, elfldr_start, "elfldr.elf") >= 0) {
reply(sender_app, false);
break;
}
#endif
reply(sender_app, true);
break;
}
case BREW_UTIL_DOWNLOAD_CHEATS: {
if(!check_for_new_commit()){
etaHEN_log("Failed to check for new commit or is up to date");
reply(sender_app, false);
break;
}
notify(true, "Downloading the latest etaHEN Cheats repo....");
if (!download_file("https://api.github.com/repos/etaHEN/PS5_Cheats/zipball",
"/data/etaHEN/cheats.zip")) {
etaHEN_log("Failed to download cheats");
reply(sender_app, true);
break;
}
etaHEN_log("Extracting Zip to the cheats folder");
if (!extract_zip("/data/etaHEN/cheats.zip", "/data/etaHEN/cheats")) {
etaHEN_log("Failed to extract zip");
reply(sender_app, true);
break;
}
unlink("/data/etaHEN/cheats.zip");
MakeInitialCheatCache(NULL);
notify(true, "Successfully updated & refreshed the etaHEN Cheats with the latest cheats repo");
reply(sender_app, false);
break;
}
case BREW_UTIL_DOWNLOAD_KSTUFF: {
notify(true, "Attempting to Download kstuff ...");
if (!download_file("https://github.com/EchoStretch/kstuff/releases/latest/download/kstuff.elf",
"/data/etaHEN/kstuff.elf")) {
unlink("/data/etaHEN/kstuff.elf");
etaHEN_log("Failed to download kstuff");
reply(sender_app, true);
break;
}
notify(true, "Successfully downloaded latest kstuff");
reply(sender_app, false);
break;
}
case BREW_UTIL_RELOAD_CHEATS: {
notify(true, "Reloading cheats cache");
ReloadCheatsCache(NULL);
reply(sender_app, false);
break;
}
case BREW_UTIL_TOGGLE_LEGACY_CMD_SERVER: {
bool turn_on = (bool)json_getInteger(json_getProperty(my_json, "toggle"));
etaHEN_log("Legacy Command Server toggle: %d", turn_on);
if (turn_on) {
notify(true, "Legacy Command Server Enabled");
global_conf.legacy_cmd_server = true;
global_conf.legacy_cmd_server_exit = true;
} else {
// dont exit server because its used to detect rest mode too
// just stop handling commands
global_conf.legacy_cmd_server = false;
notify(true, "Legacy Command Server Disabled");
}
reply(sender_app, false);
break;
}
case BREW_KILL_DAEMON:{
is_handler_enabled = false;
exit(1337);
kill(getpid(), SIGKILL);
reply(sender_app, false);
break;
}
case BREW_RELOAD_SETTINGS: {
LoadSettings();
//notify(true, "Reloaded Settings");
reply(sender_app, false);
break;
}
default:
notify(true, "Unknown command 0x%X", command);
reply(sender_app, true);
break;
}
}
void *ipc_client(void *args) {
struct clientArgs *client = (struct clientArgs *)args;
etaHEN_log("[Daemon IPC] Thread created for Socket %i", client->socket);
uint32_t readSize = 0;
IPCMessage ipcMessage; // Create an IPCMessage struct to store received data
while ((readSize = networkReceiveData(client->socket,
reinterpret_cast(&ipcMessage),
sizeof(ipcMessage))) > 0) {
if (ipcMessage.magic == 0xDEADBABE) {
// Handle IPCMessage
std::string message = ipcMessage.msg; // Retrieve the std::string message
handleIPC(client, message, ipcMessage.cmd);
} else {
etaHEN_log("[Daemon IPC][client %i] Invalid magic number",
client->cl_nmb);
ipcMessage.error = -1;
networkSendData(client->socket, reinterpret_cast(&ipcMessage),
sizeof(ipcMessage));
}
}
etaHEN_log(
"[Daemon IPC][client %i] IPC Connection disconnected, Shutting down ...",
client->cl_nmb);
networkCloseConnection(client->socket);
delete client;
pthread_exit(NULL);
return NULL;
}
void *IPC_loop(void *args) {
// Listen on port
int serverSocket = networkListen(UTIL_IPC_SOC);
if (serverSocket < 0) {
etaHEN_log("[Daemon IPC] networkListen error %s", strerror(errno));
return nullptr;
}
// Keep accepting client connections
int cli_new = 0;
while (true) {
// Accept a client connection
int clientSocket = networkAccept(serverSocket);
if (clientSocket < 0) {
etaHEN_log("[Daemon IPC] networkAccept error %s", strerror(errno));
break; // Breaking out of the loop on error to cleanup
}
etaHEN_log("[Daemon IPC] Connection Accepted");
etaHEN_log("[Daemon IPC] cl_nmb %i", cli_new);
// Build data to send to thread
auto clientParams = new clientArgs();
clientParams->ip = "localhost";
clientParams->socket = clientSocket;
clientParams->cl_nmb = cli_new;
etaHEN_log("[Daemon IPC] clientParams->cl_nmb %i", clientParams->cl_nmb);
pthread_t ipc_thread;
pthread_create(&ipc_thread, NULL, ipc_client, clientParams);
pthread_detach(ipc_thread); // Detach the thread to allow it to run independently
cli_new++;
}
// Cleanup
networkCloseConnection(serverSocket);
return nullptr;
}