diff --git a/Makefile b/Makefile index 7482642..2d4f5d6 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,11 @@ INCLUDES := -Iinclude -I$(TARGET)/include LIBS := -L$(TARGET)/lib -lcurl -lmbedtls -lmbedx509 -lmbedcrypto -lmicrohttpd -lpthread -lSceNetCtl -lSceUserService -lSceSystemService -lSceAppInstUtil -lSceHttp2 -lSceSsl -lSceNet # Source Files -SRCS := src/main.c src/payload_mgr.c src/ps5_launcher.c src/notification.c src/utils.c src/autoload.c src/app_installer.c +SRCS := src/main.c src/http_server.c src/config.c src/log_server.c \ + src/sha256.c src/json_helpers.c src/repository.c src/sources.c \ + src/payload_mgr.c src/process_mgr.c \ + src/ps5_launcher.c src/notification.c src/utils.c src/autoload.c \ + src/app_installer.c OBJS := $(SRCS:.c=.o) ELF := pldmgr.elf diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..ec06e99 --- /dev/null +++ b/include/config.h @@ -0,0 +1,48 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#include + +/* Consolidated configuration state for pldmgr_config.txt */ +typedef struct PldmgrConfig { + int autoload_enabled; /* Default: 0 (off) */ + long last_repository_update; /* Default: 0 */ + int auto_browser_open; /* Default: 1 (on) */ + int autoload_delay; /* Default: 5 (seconds) */ + int kill_disc_player; /* Default: 1 (on) */ + int scan_usb_payloads; /* Default: 0 (off) */ + int auto_install_app; /* Default: 1 (on) */ + int multi_sources_enabled; /* Default: 0 (off) */ +} PldmgrConfig; + +/* Read all config values from PLDMGR_CONFIG_PATH. + * Missing file / keys → defaults above. */ +void config_read(PldmgrConfig *cfg); + +/* Write all config values to PLDMGR_CONFIG_PATH. + * Returns 0 on success, -1 on failure. */ +int config_write(const PldmgrConfig *cfg); + +/* Read a single boolean (0/1) key from the config file. + * Returns default_val if the key or file is missing. */ +int config_read_bool(const char *key, int default_val); + +/* Apply a JSON config update sent from the frontend /set_config route. + * Handles AUTOLOAD_ENABLED, AUTO_BROWSER_OPEN, AUTOLOAD_DELAY, + * KILL_DISC_PLAYER_ON_STARTUP, SCAN_USB_PAYLOADS, AUTO_INSTALL_APP, + * MULTI_SOURCES_ENABLED, and AUTOLOAD_LIST. + * Returns 0 on success, -1 on error. */ +int config_handle_set_json(const char *json_data); + +/* Upsert a single key=value line in the config file. + * Returns 0 on success, -1 on failure. */ +int config_upsert_value(const char *key, const char *value); + +/* Read the LAST_REPOSITORY_UPDATE timestamp from the config file. + * Always succeeds; sets *out_ts to 0 if not found. */ +int config_read_last_update(long *out_ts); + +/* Write the LAST_REPOSITORY_UPDATE timestamp to the config file. */ +int config_write_last_update(long ts); + +#endif diff --git a/include/http_server.h b/include/http_server.h new file mode 100644 index 0000000..e18c3e3 --- /dev/null +++ b/include/http_server.h @@ -0,0 +1,12 @@ +#ifndef HTTP_SERVER_H +#define HTTP_SERVER_H + +#include + +/* MHD request handler callback — dispatches all routes. */ +enum MHD_Result http_on_request(void *cls, struct MHD_Connection *conn, + const char *url, const char *method, + const char *version, const char *upload_data, + size_t *upload_data_size, void **con_cls); + +#endif diff --git a/include/json_helpers.h b/include/json_helpers.h new file mode 100644 index 0000000..74197cf --- /dev/null +++ b/include/json_helpers.h @@ -0,0 +1,35 @@ +#ifndef JSON_HELPERS_H +#define JSON_HELPERS_H + +#include + +/* Incremental JSON array/object builder */ +typedef struct JsonListBuilder { + char *buf; + size_t size; + size_t pos; + int first; +} JsonListBuilder; + +/* Append formatted text to the builder. Returns 0 on success, -1 on overflow. */ +int json_append(JsonListBuilder *jb, const char *fmt, ...); + +/* Extract a quoted string value for a given key from a JSON object bounded by + * [obj_start, obj_end). Returns 0 on success, -1 if key not found. */ +int json_extract_string(const char *obj_start, const char *obj_end, + const char *key, char *out, size_t out_size); + +/* Read an entire file into a malloc'd buffer. Caller must free *out_buf. + * Returns 0 on success, -1 on failure. */ +int read_file_text(const char *path, char **out_buf, size_t *out_size); + +/* Write data to a file atomically. Returns 0 on success, -1 on failure. */ +int write_file_text(const char *path, const char *data, size_t size); + +/* Create a directory if it doesn't exist. Returns 0 on success. */ +int mkdir_if_missing(const char *path); + +/* Recursively create all directories in path. Returns 0 on success. */ +int ensure_dir_recursive(const char *path); + +#endif diff --git a/include/log_server.h b/include/log_server.h new file mode 100644 index 0000000..5daa935 --- /dev/null +++ b/include/log_server.h @@ -0,0 +1,30 @@ +#ifndef LOG_SERVER_H +#define LOG_SERVER_H + +#include +#include +#include + +#define MAX_LOG_LINES 100 +#define MAX_LOG_LINE_LEN 256 + +/* SSE log stream connection state */ +struct LogSSEConnection { + int last_log_version; + int sent_initial; +}; + +/* MHD content reader callback for SSE log streaming */ +ssize_t log_stream_callback(void *cls, uint64_t pos, char *buf, size_t max); + +/* MHD free callback for SSE connections */ +void log_stream_cleanup(void *cls); + +/* Build the /log JSON response into buf. Returns bytes written. */ +size_t log_build_json(char *buf, size_t buf_size); + +/* Track whether any browser client has connected */ +void log_server_set_active(void); +/* pldmgr_server_is_active() and pldmgr_log() are declared in pldmgr.h */ + +#endif diff --git a/include/payload_mgr.h b/include/payload_mgr.h index 243ae7d..b968060 100644 --- a/include/payload_mgr.h +++ b/include/payload_mgr.h @@ -3,41 +3,27 @@ #include -/* - * Scans directories and populates json_buf with a JSON array of payloads. - * Returns the length of the string written. +/* + * Scans directories and populates json_buf with a JSON array of payloads. + * Returns the length of the string written. */ size_t payload_mgr_list_json(char *json_buf, size_t buf_size); int payload_mgr_resolve_path(const char *filename, char *out_path, size_t out_size); int payload_mgr_delete_payload_file(const char *filename); -int payload_mgr_repository_ensure_fresh(int force_refresh); -size_t payload_mgr_repository_list_json(char *json_buf, size_t buf_size, int force_refresh); -int payload_mgr_repository_install_download(const char *filename, const char *install_source_detail, char *msg_buf, size_t msg_buf_size); -int payload_mgr_repository_install_commit(const char *filename, const char *temp_path, const char *install_source, const char *install_source_detail, char *msg_buf, size_t msg_buf_size); -int payload_mgr_import_to_storage(const char *filename, const char *temp_path, const char *install_source, const char *install_source_detail, char *msg_buf, size_t msg_buf_size); +int payload_mgr_import_to_storage(const char *filename, const char *temp_path, + const char *install_source, const char *install_source_detail, + char *msg_buf, size_t msg_buf_size); int payload_mgr_check_existing(const char *filename, char *out_json, size_t out_size); -int payload_mgr_write_metadata(const char *payload_path, const char *install_source, const char *install_source_detail); -int payload_mgr_repository_push_json(const char *json, size_t len); +int payload_mgr_write_metadata(const char *payload_path, const char *install_source, + const char *install_source_detail); /* USB Import */ int payload_mgr_usb_check(const char *usb_path, char *out_json, size_t out_size); -int payload_mgr_usb_move(const char *usb_path, int overwrite, char *out_json, size_t out_size); +int payload_mgr_usb_move(const char *usb_path, int overwrite, char *out_json, + size_t out_size); -int payload_mgr_check_self_update(char *out_path, size_t out_size); - -/* Sources Management */ -int payload_mgr_sources_list_json(char *buf, size_t size); -int payload_mgr_sources_save(const char *json, size_t len); -int payload_mgr_sources_add(const char *url, char *msg_buf, size_t msg_size); -int payload_mgr_sources_remove(int index, char *msg_buf, size_t msg_size); - -/* Multi-Source Repository */ -size_t payload_mgr_multi_repository_list_json(char *buf, size_t size, int force_refresh); -int payload_mgr_multi_repository_install(const char *filename, const char *source_id, const char *repo_url, char *msg, size_t msg_size); - -/* Processes Management */ -size_t payload_mgr_process_list_json(char *buf, size_t max_size); -int payload_mgr_process_kill(int pid); +/* Internal utility — exposed for use by repository.c and sources.c */ +void payload_mgr_remove_old_files(const char *dir_path, const char *new_filename); #endif diff --git a/include/pldmgr.h b/include/pldmgr.h index 8977db6..dca5c3f 100644 --- a/include/pldmgr.h +++ b/include/pldmgr.h @@ -46,10 +46,9 @@ "https://itsplk.github.io/ps5-payloads-mirror/payloads.json" #define REPOSITORY_REFRESH_INTERVAL_SEC 86400 -/* Logging */ +/* Logging (implementation in log_server.c) */ void pldmgr_log(const char *fmt, ...); int pldmgr_server_is_active(); -int pldmgr_read_config_bool(const char *key, int default_val); #include "autoload.h" #include "notification.h" diff --git a/include/process_mgr.h b/include/process_mgr.h new file mode 100644 index 0000000..82bc2ad --- /dev/null +++ b/include/process_mgr.h @@ -0,0 +1,13 @@ +#ifndef PROCESS_MGR_H +#define PROCESS_MGR_H + +#include + +/* Build a JSON list of all running processes. + * Returns bytes written. */ +size_t process_list_json(char *buf, size_t max_size); + +/* Kill a process by PID. Returns 0 on success, -1 on failure. */ +int process_kill(int pid); + +#endif diff --git a/include/repository.h b/include/repository.h new file mode 100644 index 0000000..5cd6762 --- /dev/null +++ b/include/repository.h @@ -0,0 +1,69 @@ +#ifndef REPOSITORY_H +#define REPOSITORY_H + +#include + +/* Payload entry from a repository JSON */ +typedef struct RepoPayload { + char name[128]; + char filename[256]; + char url[1024]; + char source[1024]; + char source_direct[1024]; + char description[1024]; + char last_update[64]; + char version[64]; + char checksum[65]; + char source_name[256]; +} RepoPayload; + +/* Download a URL to a local file. Returns 0 on success, -1 on failure. */ +int download_to_file(const char *url, const char *out_path); + +/* Parse a JSON string containing payload objects into a RepoPayload array. + * Caller must free *out_items. Returns 0 on success, -1 on failure. */ +int parse_repository_payloads(const char *json, RepoPayload **out_items, size_t *out_count); + +/* Write a .json sidecar file with payload metadata. + * Returns 0 on success. */ +int write_payload_details_json(const RepoPayload *item, const char *details_path, + const char *install_source, const char *install_source_detail); + +/* Write a minimal sidecar file when full RepoPayload info isn't available. */ +int write_simple_payload_details_json(const char *filename, const char *details_path, + const char *install_source, const char *install_source_detail); + +/* ── Repository cache management ────────────────────────── */ + +/* Accept a full JSON blob pushed by the browser and replace the cache. */ +int repository_push_json(const char *json, size_t len); + +/* Ensure the repository cache is up-to-date. If force_refresh, always re-download. */ +int repository_ensure_fresh(int force_refresh); + +/* Build the /repository_payloads JSON response. + * Returns bytes written. */ +size_t repository_list_json(char *json_buf, size_t buf_size, int force_refresh); + +/* Download and install a payload from the default repository. + * msg_buf receives a human-readable status message. */ +int repository_install_download(const char *filename, const char *install_source_detail, + char *msg_buf, size_t msg_buf_size); + +/* Verify a previously-uploaded file against the repository and commit it. + * msg_buf receives a human-readable status message. */ +int repository_install_commit(const char *filename, const char *uploaded_temp_path, + const char *install_source, const char *install_source_detail, + char *msg_buf, size_t msg_buf_size); + +/* ── Self-update ────────────────────────────────────────── */ + +/* Read the PLDMGR_VER: signature from an ELF binary. + * Returns 0 on success. */ +int get_elf_pldmgr_version(const char *path, char *out_version, size_t out_size); + +/* Scan the payload storage for a newer pldmgr.elf. + * If found, writes the path to out_path and returns 0. */ +int repository_check_self_update(char *out_path, size_t out_size); + +#endif diff --git a/include/sha256.h b/include/sha256.h new file mode 100644 index 0000000..efa0914 --- /dev/null +++ b/include/sha256.h @@ -0,0 +1,21 @@ +#ifndef SHA256_H +#define SHA256_H + +#include + +typedef struct SHA256_CTX { + unsigned char data[64]; + unsigned int datalen; + unsigned long long bitlen; + unsigned int state[8]; +} SHA256_CTX; + +void sha256_init(SHA256_CTX *ctx); +void sha256_update(SHA256_CTX *ctx, const unsigned char data[], size_t len); +void sha256_final(SHA256_CTX *ctx, unsigned char hash[]); + +/* Compute SHA256 of a file, writing the 64-char hex string to out_hex. + * Returns 0 on success, -1 on failure. */ +int compute_sha256_file(const char *path, char out_hex[65]); + +#endif diff --git a/include/sources.h b/include/sources.h new file mode 100644 index 0000000..43ee52b --- /dev/null +++ b/include/sources.h @@ -0,0 +1,27 @@ +#ifndef SOURCES_H +#define SOURCES_H + +#include + +/* List all configured sources as JSON. */ +int sources_list_json(char *buf, size_t size); + +/* Replace the sources list from a JSON POST body. */ +int sources_save(const char *json, size_t len); + +/* Add a new source by URL. On success, msg_buf receives the source name. */ +int sources_add(const char *url, char *msg_buf, size_t msg_size); + +/* Remove a source by index. Index 0 (default) cannot be removed. */ +int sources_remove(int index, char *msg_buf, size_t msg_size); + +/* Build multi-source repository JSON (grouped by source). + * Returns bytes written. */ +size_t sources_multi_repository_list_json(char *buf, size_t size, int force_refresh); + +/* Download and install a payload from a specific source. + * source_id identifies the source; repo_url is used as fallback detail. */ +int sources_multi_repository_install(const char *filename, const char *source_id, + const char *repo_url, char *msg, size_t msg_size); + +#endif diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..f7b8c30 --- /dev/null +++ b/src/config.c @@ -0,0 +1,249 @@ +#include +#include +#include +#include + +#include "config.h" +#include "pldmgr.h" +#include "json_helpers.h" + +/* ── Read / Write full config ──────────────────────────────── */ + +void config_read(PldmgrConfig *cfg) { + FILE *f; + char line[256]; + + /* Defaults */ + cfg->autoload_enabled = 0; + cfg->last_repository_update = 0; + cfg->auto_browser_open = 1; + cfg->autoload_delay = 5; + cfg->kill_disc_player = 1; + cfg->scan_usb_payloads = 0; + cfg->auto_install_app = 1; + cfg->multi_sources_enabled = 0; + + f = fopen(PLDMGR_CONFIG_PATH, "r"); + if (!f) + return; + + while (fgets(line, sizeof(line), f)) { + if (strncmp(line, "AUTOLOAD_ENABLED=", 17) == 0) + cfg->autoload_enabled = atoi(line + 17); + else if (strncmp(line, "LAST_REPOSITORY_UPDATE=", 23) == 0) + cfg->last_repository_update = atol(line + 23); + else if (strncmp(line, "AUTO_BROWSER_OPEN=", 18) == 0) + cfg->auto_browser_open = atoi(line + 18); + else if (strncmp(line, "AUTOLOAD_DELAY=", 15) == 0) + cfg->autoload_delay = atoi(line + 15); + else if (strncmp(line, "KILL_DISC_PLAYER_ON_STARTUP=", 28) == 0) + cfg->kill_disc_player = atoi(line + 28); + else if (strncmp(line, "SCAN_USB_PAYLOADS=", 18) == 0) + cfg->scan_usb_payloads = atoi(line + 18); + else if (strncmp(line, "AUTO_INSTALL_APP=", 17) == 0) + cfg->auto_install_app = atoi(line + 17); + else if (strncmp(line, "MULTI_SOURCES_ENABLED=", 22) == 0) + cfg->multi_sources_enabled = atoi(line + 22); + } + fclose(f); +} + +int config_write(const PldmgrConfig *cfg) { + FILE *f; + + mkdir(BASE_DATA_DIR, 0777); + f = fopen(PLDMGR_CONFIG_PATH, "w"); + if (!f) + return -1; + + fprintf(f, "AUTOLOAD_ENABLED=%d\n", cfg->autoload_enabled ? 1 : 0); + fprintf(f, "LAST_REPOSITORY_UPDATE=%ld\n", cfg->last_repository_update); + fprintf(f, "AUTO_BROWSER_OPEN=%d\n", cfg->auto_browser_open ? 1 : 0); + fprintf(f, "AUTOLOAD_DELAY=%d\n", cfg->autoload_delay); + fprintf(f, "KILL_DISC_PLAYER_ON_STARTUP=%d\n", cfg->kill_disc_player ? 1 : 0); + fprintf(f, "SCAN_USB_PAYLOADS=%d\n", cfg->scan_usb_payloads ? 1 : 0); + fprintf(f, "AUTO_INSTALL_APP=%d\n", cfg->auto_install_app ? 1 : 0); + fprintf(f, "MULTI_SOURCES_ENABLED=%d\n", cfg->multi_sources_enabled ? 1 : 0); + fclose(f); + return 0; +} + +/* ── Single-key helpers ────────────────────────────────────── */ + +int config_read_bool(const char *key, int default_val) { + FILE *f = fopen(PLDMGR_CONFIG_PATH, "r"); + if (!f) return default_val; + char line[256]; + int res = default_val; + size_t key_len = strlen(key); + while (fgets(line, sizeof(line), f)) { + if (strncmp(line, key, key_len) == 0 && line[key_len] == '=') { + res = atoi(line + key_len + 1); + break; + } + } + fclose(f); + return res; +} + +int config_upsert_value(const char *key, const char *value) { + FILE *f; + char line[256]; + char old_lines[64][256]; + int line_count = 0; + int replaced = 0; + size_t key_len = strlen(key); + + ensure_dir_recursive(BASE_DATA_DIR); + + f = fopen(PLDMGR_CONFIG_PATH, "r"); + if (f) { + while (line_count < 64 && fgets(line, sizeof(line), f)) { + if (strncmp(line, key, key_len) == 0 && line[key_len] == '=') { + snprintf(old_lines[line_count], sizeof(old_lines[line_count]), + "%s=%s\n", key, value); + replaced = 1; + } else { + strncpy(old_lines[line_count], line, sizeof(old_lines[line_count])); + old_lines[line_count][sizeof(old_lines[line_count]) - 1] = '\0'; + } + line_count++; + } + fclose(f); + } + + if (!replaced && line_count < 64) { + snprintf(old_lines[line_count], sizeof(old_lines[line_count]), + "%s=%s\n", key, value); + line_count++; + } + + f = fopen(PLDMGR_CONFIG_PATH, "w"); + if (!f) + return -1; + for (int i = 0; i < line_count; i++) { + fputs(old_lines[i], f); + } + fclose(f); + return 0; +} + +int config_read_last_update(long *out_ts) { + FILE *f; + char line[256]; + long ts = 0; + + *out_ts = 0; + f = fopen(PLDMGR_CONFIG_PATH, "r"); + if (!f) + return 0; + + while (fgets(line, sizeof(line), f)) { + if (strncmp(line, "LAST_REPOSITORY_UPDATE=", 23) == 0) { + ts = atol(line + 23); + break; + } + } + fclose(f); + + *out_ts = ts; + return 0; +} + +int config_write_last_update(long ts) { + char ts_buf[64]; + snprintf(ts_buf, sizeof(ts_buf), "%ld", ts); + return config_upsert_value("LAST_REPOSITORY_UPDATE", ts_buf); +} + +/* ── JSON config update handler (from /set_config POST body) ── */ + +/* Helper: extract a JSON boolean value for a given key. + * Returns 1 for true, 0 for false, or unchanged if key not found. */ +static int extract_json_bool(const char *json, const char *key, int current) { + char search[64]; + snprintf(search, sizeof(search), "\"%s\"", key); + char *pos = strstr(json, search); + if (!pos) return current; + char *val = strchr(pos, ':'); + if (!val) return current; + val++; + while (*val == ' ') val++; + if (strncmp(val, "true", 4) == 0) return 1; + if (strncmp(val, "false", 5) == 0) return 0; + return current; +} + +static int extract_json_int(const char *json, const char *key, int current) { + char search[64]; + snprintf(search, sizeof(search), "\"%s\"", key); + char *pos = strstr(json, search); + if (!pos) return current; + char *val = strchr(pos, ':'); + if (!val) return current; + val++; + while (*val == ' ') val++; + return atoi(val); +} + +int config_handle_set_json(const char *json_data) { + if (!json_data) + return -1; + + /* Read existing config */ + PldmgrConfig cfg; + config_read(&cfg); + + /* Check if AUTOLOAD_ENABLED is present */ + int enabled = extract_json_bool(json_data, "AUTOLOAD_ENABLED", -1); + if (enabled != -1) { + cfg.autoload_enabled = enabled; + cfg.auto_browser_open = extract_json_bool(json_data, "AUTO_BROWSER_OPEN", cfg.auto_browser_open); + cfg.autoload_delay = extract_json_int(json_data, "AUTOLOAD_DELAY", cfg.autoload_delay); + cfg.kill_disc_player = extract_json_bool(json_data, "KILL_DISC_PLAYER_ON_STARTUP", cfg.kill_disc_player); + cfg.scan_usb_payloads = extract_json_bool(json_data, "SCAN_USB_PAYLOADS", cfg.scan_usb_payloads); + cfg.auto_install_app = extract_json_bool(json_data, "AUTO_INSTALL_APP", cfg.auto_install_app); + cfg.multi_sources_enabled = extract_json_bool(json_data, "MULTI_SOURCES_ENABLED", cfg.multi_sources_enabled); + + if (config_write(&cfg) == 0) { + pldmgr_log("[PLDMGR] Saved config to %s\n", PLDMGR_CONFIG_PATH); + } + if (enabled == 0) + pldmgr_autoload_abort(); + } + + /* Handle AUTOLOAD_LIST if present */ + char *list_start = strstr(json_data, "\"AUTOLOAD_LIST\""); + if (list_start) { + char *val = strchr(list_start, ':'); + if (val) { + val++; + while (*val == ' ' || *val == '\"') + val++; + + char *list_end = strchr(val, '\"'); + size_t list_len = list_end ? (size_t)(list_end - val) : 0; + char *list_val = malloc(list_len + 1); + if (list_val) { + memcpy(list_val, val, list_len); + list_val[list_len] = '\0'; + + mkdir(BASE_DATA_DIR, 0777); + FILE *f = fopen(AUTOLOAD_CONFIG_PATH, "w"); + if (f) { + char *token = strtok(list_val, ","); + while (token) { + fprintf(f, "%s\n", token); + token = strtok(NULL, ","); + } + fclose(f); + pldmgr_log("[PLDMGR] Saved autoload list to %s\n", + AUTOLOAD_CONFIG_PATH); + } + free(list_val); + } + } + } + + return 0; +} diff --git a/src/http_server.c b/src/http_server.c new file mode 100644 index 0000000..edeb4a2 --- /dev/null +++ b/src/http_server.c @@ -0,0 +1,957 @@ +/* + * HTTP Server — Route dispatch for all API and asset endpoints. + * + * Contains the MHD on_request callback and all route-handling logic + * previously embedded in main.c. + */ + +#include +#include +#include +#include +#include +#include + +#include "http_server.h" +#include "pldmgr.h" +#include "log_server.h" +#include "config.h" +#include "payload_mgr.h" +#include "repository.h" +#include "sources.h" +#include "process_mgr.h" +#include "ps5_launcher.h" + +#include "assets_index_html.h" +#include "assets_cache_appcache.h" +#include "assets_favicon_svg.h" +#include "assets_icon_png.h" + +#define RESPONSE_BUFFER_SIZE 65536 +#define CORS_ORIGIN "*" + +/* Shared flag — set from main() shutdown route, read by main loop */ +volatile int http_keep_running = 1; + +/* ── Helpers ───────────────────────────────────────────────── */ + +static void add_cors_headers(struct MHD_Response *resp) { + MHD_add_response_header(resp, "Access-Control-Allow-Origin", CORS_ORIGIN); +} + +static struct MHD_Response *alloc_response_buffer(char **out) { + *out = malloc(RESPONSE_BUFFER_SIZE); + if (*out) + return NULL; + static const char oom[] = "{\"error\":\"Out of memory\"}"; + struct MHD_Response *resp = MHD_create_response_from_buffer( + sizeof(oom) - 1, (void *)oom, MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(resp, "Content-Type", "application/json"); + add_cors_headers(resp); + return resp; +} + +static int has_supported_extension(const char *path) { + const char *ext = strrchr(path, '.'); + if (!ext) return 0; + return strcasecmp(ext, ".elf") == 0 || strcasecmp(ext, ".bin") == 0; +} + +static int is_safe_filename(const char *name) { + if (!name || name[0] == '\0') return 0; + if (strstr(name, "/") || strstr(name, "..")) return 0; + return 1; +} + +static int is_allowed_payload_path(const char *path) { + if (!path || path[0] == '\0') return 0; + if (strstr(path, "..")) return 0; + if (!has_supported_extension(path)) return 0; + + for (int i = 0; i < SCAN_DIRS_COUNT; i++) { + size_t len = strlen(SCAN_DIRS[i]); + if (strncmp(path, SCAN_DIRS[i], len) == 0 && + (path[len] == '/' || path[len] == '\0')) { + return 1; + } + } + + /* Allow USB root payloads if SCAN_USB_PAYLOADS is enabled in config */ + if (strncmp(path, "/mnt/usb", 8) == 0 && + path[8] >= '0' && path[8] <= '7' && + path[9] == '/') { + if (config_read_bool("SCAN_USB_PAYLOADS", 0)) { + return 1; + } + } + + return 0; +} + +/* ── POST request state ────────────────────────────────────── */ + +struct PostStatus { + char *data; + size_t size; + int error; +}; + +struct UploadStatus { + FILE *fp; + size_t total_size; + int error; + char filename[256]; + char temp_path[512]; + char repo_url[512]; +}; + +/* ── Main request handler ──────────────────────────────────── */ + +enum MHD_Result http_on_request(void *cls, struct MHD_Connection *conn, + const char *url, const char *method, + const char *version, const char *upload_data, + size_t *upload_data_size, void **con_cls) { + + /* Handle CORS Preflight (OPTIONS) */ + if (strcmp(method, "OPTIONS") == 0) { + struct MHD_Response *resp = + MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT); + add_cors_headers(resp); + MHD_add_response_header(resp, "Access-Control-Allow-Methods", + "GET, POST, OPTIONS"); + MHD_add_response_header(resp, "Access-Control-Allow-Headers", + "Content-Type"); + enum MHD_Result ret = MHD_queue_response(conn, MHD_HTTP_OK, resp); + MHD_destroy_response(resp); + return ret; + } + + /* Set flag that we received a request (meaning browser is open) */ + log_server_set_active(); + + /* ── Initial call for a new request ────────────────────── */ + if (*con_cls == NULL) { + if (strncmp(url, ROUTE_UPLOAD, strlen(ROUTE_UPLOAD)) == 0) { + struct UploadStatus *status = malloc(sizeof(struct UploadStatus)); + status->fp = NULL; + status->total_size = 0; + status->error = 0; + + const char *filename = + MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "filename"); + if (filename && !strstr(filename, "/") && !strstr(filename, "..")) { + char path[512]; + mkdir(BASE_DATA_DIR, 0777); + mkdir(PAYLOADS_STORAGE_DIR, 0777); + snprintf(path, sizeof(path), "%s/%s.tmp", PAYLOADS_STORAGE_DIR, filename); + + strncpy(status->filename, filename, sizeof(status->filename) - 1); + status->filename[sizeof(status->filename) - 1] = '\0'; + strncpy(status->temp_path, path, sizeof(status->temp_path) - 1); + status->temp_path[sizeof(status->temp_path) - 1] = '\0'; + + status->fp = fopen(path, "wb"); + if (!status->fp) { + pldmgr_log("[PLDMGR] !!! FAILED to open temp file: %s\n", path); + status->error = 1; + } else { + pldmgr_log("[PLDMGR] Starting upload to temp: %s\n", path); + } + } else { + pldmgr_log("[PLDMGR] !!! Upload failed: Missing filename parameter\n"); + status->error = 1; + } + *con_cls = status; + return MHD_YES; + } + + if (strcmp(url, ROUTE_SET_CONFIG) == 0 && strcmp(method, "POST") == 0) { + struct PostStatus *status = malloc(sizeof(struct PostStatus)); + status->data = NULL; + status->size = 0; + status->error = 0; + *con_cls = status; + return MHD_YES; + } + + if (strcmp(url, ROUTE_REPO_PUSH) == 0 && strcmp(method, "POST") == 0) { + struct PostStatus *status = malloc(sizeof(struct PostStatus)); + status->data = NULL; + status->size = 0; + status->error = 0; + *con_cls = status; + return MHD_YES; + } + + if (strcmp(url, ROUTE_SOURCES_SET) == 0 && strcmp(method, "POST") == 0) { + struct PostStatus *status = malloc(sizeof(struct PostStatus)); + status->data = NULL; + status->size = 0; + status->error = 0; + *con_cls = status; + return MHD_YES; + } + + if (strncmp(url, ROUTE_REPO_INSTALL_PUSH, + strlen(ROUTE_REPO_INSTALL_PUSH)) == 0 && + strcmp(method, "POST") == 0) { + struct UploadStatus *status = malloc(sizeof(struct UploadStatus)); + status->fp = NULL; + status->total_size = 0; + status->error = 0; + const char *filename = + MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "filename"); + if (filename && !strstr(filename, "/") && !strstr(filename, "..")) { + char path[512]; + mkdir(BASE_DATA_DIR, 0777); + mkdir(PAYLOADS_STORAGE_DIR, 0777); + snprintf(path, sizeof(path), "%s/%s.part", PAYLOADS_STORAGE_DIR, filename); + strncpy(status->filename, filename, sizeof(status->filename) - 1); + status->filename[sizeof(status->filename) - 1] = '\0'; + strncpy(status->temp_path, path, sizeof(status->temp_path) - 1); + status->temp_path[sizeof(status->temp_path) - 1] = '\0'; + + const char *repo_url = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "repo_url"); + if (repo_url) { + strncpy(status->repo_url, repo_url, sizeof(status->repo_url) - 1); + status->repo_url[sizeof(status->repo_url) - 1] = '\0'; + } else { + strncpy(status->repo_url, REPOSITORY_SOURCE_URL, sizeof(status->repo_url) - 1); + status->repo_url[sizeof(status->repo_url) - 1] = '\0'; + } + + status->fp = fopen(path, "wb"); + if (!status->fp) { + pldmgr_log("[PLDMGR] !!! Failed to open for install: %s\n", path); + status->error = 1; + } else { + pldmgr_log("[PLDMGR] Installing payload: %s\n", path); + } + } else { + pldmgr_log("[PLDMGR] !!! install_push: invalid filename\n"); + status->error = 1; + } + *con_cls = status; + return MHD_YES; + } + + *con_cls = (void *)1; + return MHD_YES; + } + + /* ── Handle POST data for /set_config ──────────────────── */ + if (strcmp(url, ROUTE_SET_CONFIG) == 0 && strcmp(method, "POST") == 0) { + struct PostStatus *status = (struct PostStatus *)*con_cls; + if (*upload_data_size != 0) { + char *new_data = realloc(status->data, status->size + *upload_data_size + 1); + if (!new_data) { + status->error = 1; + } else { + status->data = new_data; + memcpy(status->data + status->size, upload_data, *upload_data_size); + status->size += *upload_data_size; + status->data[status->size] = '\0'; + } + *upload_data_size = 0; + return MHD_YES; + } else { + /* Finished receiving JSON */ + pldmgr_log("[PLDMGR] Received config update: %s\n", + status->data ? status->data : "(null)"); + + if (status->data && !status->error) { + config_handle_set_json(status->data); + } + + if (status->data) + free(status->data); + free(status); + *con_cls = NULL; + + struct MHD_Response *resp = MHD_create_response_from_buffer( + strlen(MSG_OK), (void *)MSG_OK, MHD_RESPMEM_MUST_COPY); + add_cors_headers(resp); + enum MHD_Result ret = MHD_queue_response(conn, MHD_HTTP_OK, resp); + MHD_destroy_response(resp); + return ret; + } + } + + /* ── Handle POST data for /repository_push ─────────────── */ + if (strcmp(url, ROUTE_REPO_PUSH) == 0 && strcmp(method, "POST") == 0) { + struct PostStatus *status = (struct PostStatus *)*con_cls; + if (*upload_data_size != 0) { + char *nd = realloc(status->data, status->size + *upload_data_size + 1); + if (!nd) { + status->error = 1; + } else { + status->data = nd; + memcpy(status->data + status->size, upload_data, *upload_data_size); + status->size += *upload_data_size; + status->data[status->size] = '\0'; + } + *upload_data_size = 0; + return MHD_YES; + } else { + int ok = -1; + if (status->data && !status->error) + ok = repository_push_json(status->data, status->size); + if (status->data) + free(status->data); + free(status); + *con_cls = NULL; + + /* Return the current list JSON so the frontend refreshes in one trip */ + char *resp_buf; + struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf); + if (oom_resp) { + enum MHD_Result ret2 = MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp); + MHD_destroy_response(oom_resp); + return ret2; + } + + size_t len = repository_list_json(resp_buf, RESPONSE_BUFFER_SIZE, 0); + struct MHD_Response *resp = MHD_create_response_from_buffer( + len, (void *)resp_buf, MHD_RESPMEM_MUST_FREE); + MHD_add_response_header(resp, "Content-Type", "application/json"); + add_cors_headers(resp); + enum MHD_Result ret2 = MHD_queue_response( + conn, ok == 0 ? MHD_HTTP_OK : MHD_HTTP_BAD_REQUEST, resp); + MHD_destroy_response(resp); + return ret2; + } + } + + /* ── Handle POST data for /sources_set ─────────────────── */ + if (strcmp(url, ROUTE_SOURCES_SET) == 0 && strcmp(method, "POST") == 0) { + struct PostStatus *status = (struct PostStatus *)*con_cls; + if (*upload_data_size != 0) { + char *nd = realloc(status->data, status->size + *upload_data_size + 1); + if (!nd) { + status->error = 1; + } else { + status->data = nd; + memcpy(status->data + status->size, upload_data, *upload_data_size); + status->size += *upload_data_size; + status->data[status->size] = '\0'; + } + *upload_data_size = 0; + return MHD_YES; + } else { + int ok = -1; + if (status->data && !status->error) + ok = sources_save(status->data, status->size); + if (status->data) + free(status->data); + free(status); + *con_cls = NULL; + + const char *msg = ok == 0 ? MSG_OK : "Failed to save sources"; + struct MHD_Response *resp_ss = MHD_create_response_from_buffer( + strlen(msg), (void *)msg, MHD_RESPMEM_MUST_COPY); + add_cors_headers(resp_ss); + enum MHD_Result ret_ss = MHD_queue_response( + conn, ok == 0 ? MHD_HTTP_OK : MHD_HTTP_INTERNAL_SERVER_ERROR, resp_ss); + MHD_destroy_response(resp_ss); + return ret_ss; + } + } + + /* ── Chunked upload for /manage:upload ─────────────────── */ + if (strncmp(url, ROUTE_UPLOAD, strlen(ROUTE_UPLOAD)) == 0) { + struct UploadStatus *status = (struct UploadStatus *)*con_cls; + if (*upload_data_size != 0) { + if (status->fp && !status->error) { + size_t written = fwrite(upload_data, 1, *upload_data_size, status->fp); + if (written != *upload_data_size) { + pldmgr_log("[PLDMGR] !!! Write error: expected %zu, got %zu\n", + *upload_data_size, written); + status->error = 1; + } + status->total_size += written; + } + *upload_data_size = 0; + return MHD_YES; + } else { + /* Upload finished */ + if (status->fp) { + fflush(status->fp); + fclose(status->fp); + status->fp = NULL; + + if (!status->error) { + char msg[256]; + if (payload_mgr_import_to_storage(status->filename, status->temp_path, "web_upload", "", msg, sizeof(msg)) != 0) { + pldmgr_log("[PLDMGR] !!! Failed to commit upload: %s\n", msg); + status->error = 1; + } + } + } + pldmgr_log("[PLDMGR] Upload finished. Total bytes: %zu, Error: %d\n", + status->total_size, status->error); + + int err = status->error; + free(status); + *con_cls = NULL; + + const char *msg = err ? "Error during upload\n" : MSG_OK; + struct MHD_Response *resp = MHD_create_response_from_buffer( + strlen(msg), (void *)msg, MHD_RESPMEM_MUST_COPY); + add_cors_headers(resp); + enum MHD_Result ret = MHD_queue_response( + conn, err ? MHD_HTTP_INTERNAL_SERVER_ERROR : MHD_HTTP_OK, resp); + MHD_destroy_response(resp); + return ret; + } + } + + /* ── Chunked upload for /repository_install_push ────────── */ + if (strncmp(url, ROUTE_REPO_INSTALL_PUSH, strlen(ROUTE_REPO_INSTALL_PUSH)) == 0) { + struct UploadStatus *status = (struct UploadStatus *)*con_cls; + if (*upload_data_size != 0) { + if (status->fp && !status->error) { + size_t written = fwrite(upload_data, 1, *upload_data_size, status->fp); + if (written != *upload_data_size) { + status->error = 1; + } + status->total_size += written; + } + *upload_data_size = 0; + return MHD_YES; + } else { + if (status->fp) { + fflush(status->fp); + fclose(status->fp); + } + int err = status->error; + + char msg_buf[1024] = ""; + if (!err) { + if (repository_install_commit( + status->filename, status->temp_path, "repository", status->repo_url, msg_buf, + sizeof(msg_buf)) != 0) { + err = 1; + } + } + + pldmgr_log("[PLDMGR] Payload install %s (%zu bytes)\n", + err ? "FAILED" : "complete", status->total_size); + free(status); + *con_cls = NULL; + if (err && msg_buf[0] == '\0') { + snprintf(msg_buf, sizeof(msg_buf), "Write error"); + } + char json_resp[1024]; + snprintf(json_resp, sizeof(json_resp), "{\"ok\":%s,\"message\":\"%s\"}", + err ? "false" : "true", err ? msg_buf : "Installed"); + struct MHD_Response *resp2 = MHD_create_response_from_buffer( + strlen(json_resp), (void *)json_resp, MHD_RESPMEM_MUST_COPY); + MHD_add_response_header(resp2, "Content-Type", "application/json"); + add_cors_headers(resp2); + enum MHD_Result ret2 = MHD_queue_response( + conn, err ? MHD_HTTP_INTERNAL_SERVER_ERROR : MHD_HTTP_OK, resp2); + MHD_destroy_response(resp2); + return ret2; + } + } + + /* ── Log significant requests ──────────────────────────── */ + if (strcmp(url, ROUTE_LOG) != 0 && strcmp(url, ROUTE_INDEX) != 0 && + strcmp(url, ROUTE_INDEX_HTML) != 0 && strcmp(url, "/favicon.svg") != 0 && + strcmp(url, "/icon.png") != 0) { + pldmgr_log("[PLDMGR] Request: %s %s\n", method, url); + } + + struct MHD_Response *resp = NULL; + enum MHD_Result ret; + int http_status = MHD_HTTP_OK; + + /* ── USB routes ────────────────────────────────────────── */ + if (strcmp(url, ROUTE_USB_MOVE_CHECK) == 0) { + const char *path = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "path"); + if (path) { + char *resp_buf; + struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf); + if (oom_resp) { + ret = MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp); + MHD_destroy_response(oom_resp); + return ret; + } + int rc = payload_mgr_usb_check(path, resp_buf, RESPONSE_BUFFER_SIZE); + resp = MHD_create_response_from_buffer(strlen(resp_buf), (void *)resp_buf, MHD_RESPMEM_MUST_FREE); + MHD_add_response_header(resp, "Content-Type", "application/json"); + add_cors_headers(resp); + ret = MHD_queue_response(conn, rc == 0 ? MHD_HTTP_OK : MHD_HTTP_BAD_REQUEST, resp); + MHD_destroy_response(resp); + return ret; + } else { + const char *err = "{\"error\":\"Missing path\"}"; + resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY); + MHD_add_response_header(resp, "Content-Type", "application/json"); + } + add_cors_headers(resp); + ret = MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp); + MHD_destroy_response(resp); + return ret; + } + + if (strcmp(url, ROUTE_USB_MOVE_PERFORM) == 0) { + const char *path = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "path"); + const char *overwrite_str = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "overwrite"); + int overwrite = (overwrite_str && strcmp(overwrite_str, "true") == 0) ? 1 : 0; + if (path) { + char *resp_buf; + struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf); + if (oom_resp) { + ret = MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp); + MHD_destroy_response(oom_resp); + return ret; + } + int rc = payload_mgr_usb_move(path, overwrite, resp_buf, RESPONSE_BUFFER_SIZE); + resp = MHD_create_response_from_buffer(strlen(resp_buf), (void *)resp_buf, MHD_RESPMEM_MUST_FREE); + MHD_add_response_header(resp, "Content-Type", "application/json"); + add_cors_headers(resp); + ret = MHD_queue_response(conn, rc == 0 ? MHD_HTTP_OK : MHD_HTTP_BAD_REQUEST, resp); + MHD_destroy_response(resp); + return ret; + } else { + const char *err = "{\"error\":\"Missing path\"}"; + resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY); + MHD_add_response_header(resp, "Content-Type", "application/json"); + } + add_cors_headers(resp); + ret = MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp); + MHD_destroy_response(resp); + return ret; + } + + /* ── Static assets ─────────────────────────────────────── */ + if (strcmp(url, ROUTE_INDEX) == 0 || strcmp(url, ROUTE_INDEX_HTML) == 0) { + resp = MHD_create_response_from_buffer(assets_index_html_len, + (void *)assets_index_html, + MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(resp, "Content-Type", "text/html"); + MHD_add_response_header(resp, "Cache-Control", "no-cache, must-revalidate"); + } else if (strcmp(url, ROUTE_CACHE_MANIFEST) == 0) { + resp = MHD_create_response_from_buffer(assets_cache_appcache_len, + (void *)assets_cache_appcache, + MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(resp, "Content-Type", "text/cache-manifest"); + MHD_add_response_header(resp, "Cache-Control", "no-cache, must-revalidate"); + } else if (strcmp(url, "/favicon.svg") == 0) { + resp = MHD_create_response_from_buffer(assets_favicon_svg_len, + (void *)assets_favicon_svg, + MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(resp, "Content-Type", "image/svg+xml"); + MHD_add_response_header(resp, "Cache-Control", "max-age=604800"); + } else if (strcmp(url, "/icon.png") == 0) { + resp = MHD_create_response_from_buffer(assets_icon_png_len, + (void *)assets_icon_png, + MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(resp, "Content-Type", "image/png"); + MHD_add_response_header(resp, "Cache-Control", "max-age=604800"); + + /* ── API routes ────────────────────────────────────────── */ + } else if (strcmp(url, ROUTE_CHECK) == 0) { + const char *filename = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "filename"); + if (!filename) { + const char *err = "{\"error\":\"Missing filename\"}"; + resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY); + MHD_add_response_header(resp, "Content-Type", "application/json"); + add_cors_headers(resp); + return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp); + } + if (!is_safe_filename(filename)) { + const char *err = "{\"error\":\"Invalid filename\"}"; + resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY); + MHD_add_response_header(resp, "Content-Type", "application/json"); + add_cors_headers(resp); + return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp); + } + char *resp_buf; + struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf); + if (oom_resp) + return MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp); + payload_mgr_check_existing(filename, resp_buf, RESPONSE_BUFFER_SIZE); + resp = MHD_create_response_from_buffer(strlen(resp_buf), (void *)resp_buf, MHD_RESPMEM_MUST_FREE); + MHD_add_response_header(resp, "Content-Type", "application/json"); + add_cors_headers(resp); + return MHD_queue_response(conn, MHD_HTTP_OK, resp); + } else if (strcmp(url, ROUTE_LIST_PAYLOADS) == 0) { + char *resp_buf; + struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf); + if (oom_resp) + return MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp); + size_t len = payload_mgr_list_json(resp_buf, RESPONSE_BUFFER_SIZE); + resp = MHD_create_response_from_buffer(len, (void *)resp_buf, MHD_RESPMEM_MUST_FREE); + MHD_add_response_header(resp, "Content-Type", "application/json"); + } else if (strcmp(url, ROUTE_PROCESSES_LIST) == 0) { + char *resp_buf; + struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf); + if (oom_resp) + return MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp); + size_t len = process_list_json(resp_buf, RESPONSE_BUFFER_SIZE); + resp = MHD_create_response_from_buffer(len, (void *)resp_buf, MHD_RESPMEM_MUST_FREE); + MHD_add_response_header(resp, "Content-Type", "application/json"); + } else if (strcmp(url, ROUTE_PROCESS_KILL) == 0) { + const char *pid_str = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "pid"); + if (!pid_str) { + const char *err = "{\"ok\":false,\"message\":\"Missing pid\"}"; + resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY); + MHD_add_response_header(resp, "Content-Type", "application/json"); + add_cors_headers(resp); + return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp); + } + int pid = atoi(pid_str); + int rc = process_kill(pid); + char json_resp[256]; + snprintf(json_resp, sizeof(json_resp), "{\"ok\":%s,\"message\":\"%s\"}", + rc == 0 ? "true" : "false", rc == 0 ? "Killed" : "Failed to kill"); + resp = MHD_create_response_from_buffer(strlen(json_resp), (void *)json_resp, MHD_RESPMEM_MUST_COPY); + MHD_add_response_header(resp, "Content-Type", "application/json"); + add_cors_headers(resp); + return MHD_queue_response(conn, rc == 0 ? MHD_HTTP_OK : MHD_HTTP_INTERNAL_SERVER_ERROR, resp); + } else if (strcmp(url, ROUTE_REPO_LIST) == 0) { + char *resp_buf; + struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf); + if (oom_resp) + return MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp); + PldmgrConfig cfg; + config_read(&cfg); + size_t len; + if (cfg.multi_sources_enabled) + len = sources_multi_repository_list_json(resp_buf, RESPONSE_BUFFER_SIZE, 0); + else + len = repository_list_json(resp_buf, RESPONSE_BUFFER_SIZE, 0); + resp = MHD_create_response_from_buffer(len, (void *)resp_buf, MHD_RESPMEM_MUST_FREE); + MHD_add_response_header(resp, "Content-Type", "application/json"); + } else if (strcmp(url, ROUTE_REPO_REFRESH) == 0) { + char *resp_buf; + struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf); + if (oom_resp) + return MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp); + PldmgrConfig cfg; + config_read(&cfg); + size_t len2; + if (cfg.multi_sources_enabled) + len2 = sources_multi_repository_list_json(resp_buf, RESPONSE_BUFFER_SIZE, 1); + else + len2 = repository_list_json(resp_buf, RESPONSE_BUFFER_SIZE, 1); + resp = MHD_create_response_from_buffer(len2, (void *)resp_buf, MHD_RESPMEM_MUST_FREE); + MHD_add_response_header(resp, "Content-Type", "application/json"); + } else if (strcmp(url, ROUTE_REPO_INSTALL) == 0) { + const char *filename = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "filename"); + const char *repo_url = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "repo_url"); + const char *source_id = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "source_id"); + if (!filename) { + const char *err = "{\"ok\":false,\"message\":\"Missing filename\"}"; + resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY); + MHD_add_response_header(resp, "Content-Type", "application/json"); + add_cors_headers(resp); + return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp); + } + if (!is_safe_filename(filename)) { + const char *err = "{\"ok\":false,\"message\":\"Invalid filename\"}"; + resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY); + MHD_add_response_header(resp, "Content-Type", "application/json"); + add_cors_headers(resp); + return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp); + } + + char msg_buf[1024] = ""; + int rc; + if (source_id && source_id[0]) { + rc = sources_multi_repository_install(filename, source_id, + repo_url ? repo_url : "", + msg_buf, sizeof(msg_buf)); + } else { + const char *detail = (repo_url && repo_url[0]) ? repo_url : REPOSITORY_SOURCE_URL; + rc = repository_install_download(filename, detail, msg_buf, sizeof(msg_buf)); + } + if (msg_buf[0] == '\0') { + snprintf(msg_buf, sizeof(msg_buf), rc == 0 ? "Installed" : "Install failed"); + } + + char json_resp[1024]; + snprintf(json_resp, sizeof(json_resp), "{\"ok\":%s,\"message\":\"%s\"}", + rc == 0 ? "true" : "false", msg_buf); + resp = MHD_create_response_from_buffer(strlen(json_resp), (void *)json_resp, MHD_RESPMEM_MUST_COPY); + MHD_add_response_header(resp, "Content-Type", "application/json"); + add_cors_headers(resp); + return MHD_queue_response(conn, rc == 0 ? MHD_HTTP_OK : MHD_HTTP_INTERNAL_SERVER_ERROR, resp); + } else if (strcmp(url, ROUTE_SOURCES_LIST) == 0) { + char *resp_buf; + struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf); + if (oom_resp) + return MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp); + sources_list_json(resp_buf, RESPONSE_BUFFER_SIZE); + resp = MHD_create_response_from_buffer(strlen(resp_buf), (void *)resp_buf, MHD_RESPMEM_MUST_FREE); + MHD_add_response_header(resp, "Content-Type", "application/json"); + } else if (strcmp(url, ROUTE_SOURCES_ADD) == 0) { + const char *src_url = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "url"); + if (!src_url || !src_url[0]) { + const char *err = "{\"ok\":false,\"message\":\"Missing url parameter\"}"; + resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY); + MHD_add_response_header(resp, "Content-Type", "application/json"); + add_cors_headers(resp); + return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp); + } + char msg_buf_src[512] = ""; + int rc_src = sources_add(src_url, msg_buf_src, sizeof(msg_buf_src)); + char json_resp_src[1024]; + if (rc_src == 0) { + char name_escaped[512]; + pldmgr_json_escape(msg_buf_src, name_escaped, sizeof(name_escaped)); + snprintf(json_resp_src, sizeof(json_resp_src), + "{\"ok\":true,\"name\":\"%s\"}", name_escaped); + } else { + char msg_escaped[512]; + pldmgr_json_escape(msg_buf_src, msg_escaped, sizeof(msg_escaped)); + snprintf(json_resp_src, sizeof(json_resp_src), + "{\"ok\":false,\"message\":\"%s\"}", msg_escaped); + } + resp = MHD_create_response_from_buffer(strlen(json_resp_src), (void *)json_resp_src, MHD_RESPMEM_MUST_COPY); + MHD_add_response_header(resp, "Content-Type", "application/json"); + add_cors_headers(resp); + return MHD_queue_response(conn, rc_src == 0 ? MHD_HTTP_OK : MHD_HTTP_BAD_REQUEST, resp); + } else if (strcmp(url, ROUTE_SOURCES_REMOVE) == 0) { + const char *idx_str = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "index"); + if (!idx_str) { + const char *err = "{\"ok\":false,\"message\":\"Missing index parameter\"}"; + resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY); + MHD_add_response_header(resp, "Content-Type", "application/json"); + add_cors_headers(resp); + return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp); + } + int idx = atoi(idx_str); + char msg_rm[256] = ""; + int rc_rm = sources_remove(idx, msg_rm, sizeof(msg_rm)); + char json_rm[512]; + char msg_rm_e[256]; + pldmgr_json_escape(msg_rm, msg_rm_e, sizeof(msg_rm_e)); + snprintf(json_rm, sizeof(json_rm), "{\"ok\":%s,\"message\":\"%s\"}", + rc_rm == 0 ? "true" : "false", msg_rm_e); + resp = MHD_create_response_from_buffer(strlen(json_rm), (void *)json_rm, MHD_RESPMEM_MUST_COPY); + MHD_add_response_header(resp, "Content-Type", "application/json"); + add_cors_headers(resp); + return MHD_queue_response(conn, rc_rm == 0 ? MHD_HTTP_OK : MHD_HTTP_BAD_REQUEST, resp); + } else if (strncmp(url, ROUTE_LOAD_PAYLOAD, strlen(ROUTE_LOAD_PAYLOAD)) == 0) { + const char *path = url + strlen(ROUTE_LOAD_PAYLOAD); + const char *final_path = NULL; + char resolved_path[512]; + if (is_allowed_payload_path(path)) { + final_path = path; + } else { + const char *filename = strrchr(path, '/'); + filename = filename ? filename + 1 : path; + if (is_safe_filename(filename) && + payload_mgr_resolve_path(filename, resolved_path, + sizeof(resolved_path)) == 0) { + final_path = resolved_path; + } + } + + if (!final_path) { + pldmgr_log("[PLDMGR] !!! Payload path rejected: %s\n", path); + const char *err = "Invalid payload name\n"; + resp = MHD_create_response_from_buffer(strlen(err), (void *)err, + MHD_RESPMEM_PERSISTENT); + http_status = MHD_HTTP_BAD_REQUEST; + } else if (ps5_launch_elf(final_path) == 0) { + resp = MHD_create_response_from_buffer(strlen(MSG_OK), (void *)MSG_OK, + MHD_RESPMEM_PERSISTENT); + } else { + const char *err = "Failed to launch payload\n"; + resp = MHD_create_response_from_buffer(strlen(err), (void *)err, + MHD_RESPMEM_PERSISTENT); + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + } + MHD_add_response_header(resp, "Content-Type", "text/plain"); + } else if (strncmp(url, ROUTE_DELETE, strlen(ROUTE_DELETE)) == 0) { + const char *filename = + MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "filename"); + if (filename) { + if (strstr(filename, "/") || strstr(filename, "..")) { + const char *err = "Invalid filename\n"; + resp = MHD_create_response_from_buffer(strlen(err), (void *)err, + MHD_RESPMEM_PERSISTENT); + } else { + if (payload_mgr_delete_payload_file(filename) == 0) { + pldmgr_log("[PLDMGR] Deleted payload: %s\n", filename); + resp = MHD_create_response_from_buffer(strlen(MSG_OK), (void *)MSG_OK, + MHD_RESPMEM_PERSISTENT); + } else { + const char *err = "Failed to delete file\n"; + resp = MHD_create_response_from_buffer(strlen(err), (void *)err, + MHD_RESPMEM_PERSISTENT); + } + } + } else { + const char *err = "Missing filename\n"; + resp = MHD_create_response_from_buffer(strlen(err), (void *)err, + MHD_RESPMEM_PERSISTENT); + } + MHD_add_response_header(resp, "Content-Type", "text/plain"); + } else if (strcmp(url, ROUTE_SHUTDOWN) == 0) { + const char *msg = "Payload Manager Core shutting down...\n"; + pldmgr_log("[PLDMGR] %s", msg); + resp = MHD_create_response_from_buffer(strlen(msg), (void *)msg, + MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(resp, "Content-Type", "text/plain"); + http_keep_running = 0; + } else if (strcmp(url, ROUTE_LOG) == 0) { + char *resp_buf; + resp = alloc_response_buffer(&resp_buf); + if (!resp) { + size_t pos = log_build_json(resp_buf, RESPONSE_BUFFER_SIZE); + resp = MHD_create_response_from_buffer(pos, (void *)resp_buf, + MHD_RESPMEM_MUST_FREE); + MHD_add_response_header(resp, "Content-Type", "application/json"); + } + } else if (strcmp(url, ROUTE_VERSION) == 0) { + resp = MHD_create_response_from_buffer( + strlen(MENU_VERSION), (void *)MENU_VERSION, MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(resp, "Content-Type", "text/plain"); + } else if (strcmp(url, ROUTE_GETIP) == 0) { + char ip[64]; + if (pldmgr_get_local_ip(ip, sizeof(ip)) != 0) { + strcpy(ip, "0.0.0.0"); + } + resp = MHD_create_response_from_buffer(strlen(ip), (void *)ip, + MHD_RESPMEM_MUST_COPY); + MHD_add_response_header(resp, "Content-Type", "text/plain"); + } else if (strcmp(url, ROUTE_AUTOLOAD_STATUS) == 0) { + int total, done; + char current[128]; + pldmgr_autoload_get_status(&total, &done, current); + int remaining = pldmgr_autoload_get_remaining_seconds(); + long long remaining_ms = pldmgr_autoload_get_remaining_ms(); + + char list_buf[4096] = ""; + FILE *f = fopen(AUTOLOAD_CONFIG_PATH, "r"); + if (f) { + char line[256]; + int first = 1; + while (fgets(line, sizeof(line), f)) { + line[strcspn(line, "\r\n")] = 0; + if (strlen(line) == 0 || line[0] == '!') + continue; + if (!first) strcat(list_buf, ","); + strcat(list_buf, line); + first = 0; + } + fclose(f); + } + + PldmgrConfig cfg; + config_read(&cfg); + + char current_escaped[256]; + char list_escaped[8192]; + pldmgr_json_escape(current, current_escaped, sizeof(current_escaped)); + pldmgr_json_escape(list_buf, list_escaped, sizeof(list_escaped)); + + char *resp_buf; + resp = alloc_response_buffer(&resp_buf); + if (!resp) { + snprintf(resp_buf, RESPONSE_BUFFER_SIZE, + "{\"remaining\":%d,\"remaining_ms\":%lld,\"total\":%d,\"done\":%d,\"current\":\"%s\"," + "\"list\":\"%s\",\"delay\":%d,\"KILL_DISC_PLAYER_ON_STARTUP\":%s,\"SCAN_USB_PAYLOADS\":%s}", + remaining, remaining_ms, total, done, current_escaped, list_escaped, cfg.autoload_delay, + cfg.kill_disc_player ? "true" : "false", cfg.scan_usb_payloads ? "true" : "false"); + resp = MHD_create_response_from_buffer(strlen(resp_buf), + (void *)resp_buf, + MHD_RESPMEM_MUST_FREE); + MHD_add_response_header(resp, "Content-Type", "application/json"); + } + } else if (strcmp(url, ROUTE_AUTOLOAD_CLEAR) == 0) { + pldmgr_autoload_reset(); + const char *msg = "Autoload status cleared.\n"; + resp = MHD_create_response_from_buffer(strlen(msg), (void *)msg, + MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(resp, "Content-Type", "text/plain"); + } else if (strcmp(url, ROUTE_ABORT) == 0) { + pldmgr_autoload_abort(); + const char *msg = "Autoload sequence aborted.\n"; + resp = MHD_create_response_from_buffer(strlen(msg), (void *)msg, + MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(resp, "Content-Type", "text/plain"); + } else if (strcmp(url, "/autoload_status") == 0) { + char resp_buf[64]; + snprintf(resp_buf, sizeof(resp_buf), "{\"remaining\":%d}", + pldmgr_autoload_get_remaining_seconds()); + resp = MHD_create_response_from_buffer(strlen(resp_buf), + (void *)resp_buf, + MHD_RESPMEM_MUST_COPY); + MHD_add_response_header(resp, "Content-Type", "application/json"); + } else if (strcmp(url, ROUTE_GET_CONFIG) == 0) { + PldmgrConfig cfg; + config_read(&cfg); + + /* Get list */ + char list_buf[4096] = {0}; + FILE *f = fopen(AUTOLOAD_CONFIG_PATH, "r"); + if (f) { + char line[256]; + int first = 1; + while (fgets(line, sizeof(line), f)) { + line[strcspn(line, "\r\n")] = 0; + if (strlen(line) == 0) + continue; + if (!first) + strncat(list_buf, ",", sizeof(list_buf) - strlen(list_buf) - 1); + strncat(list_buf, line, sizeof(list_buf) - strlen(list_buf) - 1); + first = 0; + } + fclose(f); + } + + char list_escaped[8192]; + pldmgr_json_escape(list_buf, list_escaped, sizeof(list_escaped)); + + char *resp_buf; + resp = alloc_response_buffer(&resp_buf); + if (!resp) { + snprintf(resp_buf, RESPONSE_BUFFER_SIZE, + "{\"AUTOLOAD_ENABLED\":%s,\"AUTOLOAD_LIST\":\"%s\",\"LAST_" + "REPOSITORY_UPDATE\":%ld," + "\"AUTO_BROWSER_OPEN\":%s,\"AUTOLOAD_DELAY\":%d,\"KILL_DISC_" + "PLAYER_ON_STARTUP\":%s,\"SCAN_USB_PAYLOADS\":%s,\"AUTO_INSTALL_APP\":%s," + "\"MULTI_SOURCES_ENABLED\":%s}", + cfg.autoload_enabled ? "true" : "false", list_escaped, cfg.last_repository_update, + cfg.auto_browser_open ? "true" : "false", cfg.autoload_delay, + cfg.kill_disc_player ? "true" : "false", cfg.scan_usb_payloads ? "true" : "false", + cfg.auto_install_app ? "true" : "false", + cfg.multi_sources_enabled ? "true" : "false"); + resp = MHD_create_response_from_buffer(strlen(resp_buf), + (void *)resp_buf, + MHD_RESPMEM_MUST_FREE); + MHD_add_response_header(resp, "Content-Type", "application/json"); + } + } else if (strcmp(url, "/events") == 0) { + struct LogSSEConnection *sse = malloc(sizeof(struct LogSSEConnection)); + sse->last_log_version = 0; + sse->sent_initial = 0; + + resp = MHD_create_response_from_callback( + MHD_SIZE_UNKNOWN, 1024, &log_stream_callback, sse, &log_stream_cleanup); + MHD_add_response_header(resp, "Content-Type", "text/event-stream"); + MHD_add_response_header(resp, "Cache-Control", "no-cache"); + MHD_add_response_header(resp, "Connection", "keep-alive"); + } else { + /* Default: 404 */ + const char *not_found = "404 Not Found\n"; + resp = MHD_create_response_from_buffer(strlen(not_found), (void *)not_found, + MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(resp, "Content-Type", "text/plain"); + } + + if (!resp) + return MHD_NO; + + add_cors_headers(resp); + ret = MHD_queue_response(conn, http_status, resp); + MHD_destroy_response(resp); + + return ret; +} diff --git a/src/json_helpers.c b/src/json_helpers.c new file mode 100644 index 0000000..187d613 --- /dev/null +++ b/src/json_helpers.c @@ -0,0 +1,165 @@ +#include +#include +#include +#include +#include +#include +#include "json_helpers.h" + +int json_append(JsonListBuilder *jb, const char *fmt, ...) { + va_list args; + int written; + + if (jb->pos >= jb->size) { + return -1; + } + + va_start(args, fmt); + written = vsnprintf(jb->buf + jb->pos, jb->size - jb->pos, fmt, args); + va_end(args); + + if (written < 0) { + return -1; + } + + if ((size_t)written >= jb->size - jb->pos) { + jb->pos = jb->size - 1; + jb->buf[jb->pos] = '\0'; + return -1; + } + + jb->pos += (size_t)written; + return 0; +} + +int json_extract_string(const char *obj_start, const char *obj_end, + const char *key, char *out, size_t out_size) { + char key_pattern[96]; + const char *p; + const char *colon; + const char *q; + size_t pos = 0; + + if (out_size == 0) { + return -1; + } + out[0] = '\0'; + + snprintf(key_pattern, sizeof(key_pattern), "\"%s\"", key); + p = strstr(obj_start, key_pattern); + if (!p || p >= obj_end) { + return -1; + } + + colon = strchr(p + strlen(key_pattern), ':'); + if (!colon || colon >= obj_end) { + return -1; + } + + q = colon + 1; + while (q < obj_end && isspace((unsigned char)*q)) { + q++; + } + if (q >= obj_end || *q != '"') { + return -1; + } + q++; + + while (q < obj_end && *q != '"') { + if (*q == '\\' && (q + 1) < obj_end) { + q++; + } + if (pos + 1 < out_size) { + out[pos++] = *q; + } + q++; + } + out[pos] = '\0'; + return 0; +} + +int read_file_text(const char *path, char **out_buf, size_t *out_size) { + FILE *f; + long fsize; + char *buf; + size_t nread; + + *out_buf = NULL; + *out_size = 0; + + f = fopen(path, "rb"); + if (!f) { + return -1; + } + + if (fseek(f, 0, SEEK_END) != 0) { + fclose(f); + return -1; + } + fsize = ftell(f); + if (fsize < 0) { + fclose(f); + return -1; + } + if (fseek(f, 0, SEEK_SET) != 0) { + fclose(f); + return -1; + } + + buf = (char *)malloc((size_t)fsize + 1); + if (!buf) { + fclose(f); + return -1; + } + + nread = fread(buf, 1, (size_t)fsize, f); + fclose(f); + buf[nread] = '\0'; + + *out_buf = buf; + *out_size = nread; + return 0; +} + +int write_file_text(const char *path, const char *data, size_t size) { + FILE *f = fopen(path, "wb"); + if (!f) { + return -1; + } + if (fwrite(data, 1, size, f) != size) { + fclose(f); + return -1; + } + fclose(f); + return 0; +} + +int mkdir_if_missing(const char *path) { + struct stat st; + if (stat(path, &st) == 0) { + return S_ISDIR(st.st_mode) ? 0 : -1; + } + return mkdir(path, 0777); +} + +int ensure_dir_recursive(const char *path) { + char tmp[512]; + size_t len = strlen(path); + if (len >= sizeof(tmp)) { + return -1; + } + + strncpy(tmp, path, sizeof(tmp)); + tmp[len] = '\0'; + + for (size_t i = 1; i < len; i++) { + if (tmp[i] == '/') { + tmp[i] = '\0'; + if (strlen(tmp) > 0 && mkdir_if_missing(tmp) != 0) { + return -1; + } + tmp[i] = '/'; + } + } + return mkdir_if_missing(tmp); +} diff --git a/src/log_server.c b/src/log_server.c new file mode 100644 index 0000000..060fe73 --- /dev/null +++ b/src/log_server.c @@ -0,0 +1,128 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "log_server.h" +#include "utils.h" + +/* Ring buffer state */ +static char log_buffer[MAX_LOG_LINES][MAX_LOG_LINE_LEN]; +static int log_head = 0; +static int log_count = 0; +static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t log_cond = PTHREAD_COND_INITIALIZER; +static int log_updated = 0; + +static volatile int server_active_flag = 0; + +void log_server_set_active(void) { server_active_flag = 1; } +int pldmgr_server_is_active(void) { return server_active_flag; } + +void pldmgr_log(const char *fmt, ...) { + char line[MAX_LOG_LINE_LEN]; + va_list args; + va_start(args, fmt); + vsnprintf(line, sizeof(line), fmt, args); + va_end(args); + + /* Print to stdout */ + printf("%s", line); + + /* Remove trailing newline for internal storage if present */ + size_t len = strlen(line); + while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) { + line[len - 1] = '\0'; + len--; + } + + if (len == 0) + return; + + /* Add to circular buffer with lock */ + pthread_mutex_lock(&log_mutex); + strncpy(log_buffer[log_head], line, MAX_LOG_LINE_LEN); + log_head = (log_head + 1) % MAX_LOG_LINES; + if (log_count < MAX_LOG_LINES) + log_count++; + + /* Signal SSE clients */ + log_updated++; + pthread_cond_broadcast(&log_cond); + pthread_mutex_unlock(&log_mutex); +} + +ssize_t log_stream_callback(void *cls, uint64_t pos, char *buf, size_t max) { + struct LogSSEConnection *conn = (struct LogSSEConnection *)cls; + + pthread_mutex_lock(&log_mutex); + + /* Initial catch-up */ + if (!conn->sent_initial) { + char initial_batch[MAX_LOG_LINES * (MAX_LOG_LINE_LEN + 16)]; + size_t offset = 0; + + for (int i = 0; i < log_count; i++) { + int idx = (log_head - log_count + i + MAX_LOG_LINES) % MAX_LOG_LINES; + offset += snprintf(initial_batch + offset, sizeof(initial_batch) - offset, + "data: %s\n\n", log_buffer[idx]); + } + + conn->sent_initial = 1; + conn->last_log_version = log_updated; + + if (offset > 0) { + size_t to_copy = (offset < max) ? offset : max; + memcpy(buf, initial_batch, to_copy); + pthread_mutex_unlock(&log_mutex); + return to_copy; + } + } + + /* Wait for new logs */ + while (conn->last_log_version == log_updated) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += 5; /* Keep-alive ping interval */ + + if (pthread_cond_timedwait(&log_cond, &log_mutex, &ts) == ETIMEDOUT) { + /* Send a comment keep-alive */ + const char *ping = ": ping\n\n"; + memcpy(buf, ping, strlen(ping)); + pthread_mutex_unlock(&log_mutex); + return strlen(ping); + } + } + + /* Send new log */ + int idx = (log_head - 1 + MAX_LOG_LINES) % MAX_LOG_LINES; + size_t len = snprintf(buf, max, "data: %s\n\n", log_buffer[idx]); + conn->last_log_version = log_updated; + + pthread_mutex_unlock(&log_mutex); + return len; +} + +void log_stream_cleanup(void *cls) { free(cls); } + +size_t log_build_json(char *buf, size_t buf_size) { + size_t bpos = 0; + bpos += snprintf(buf + bpos, buf_size - bpos, "{\"logs\":["); + + pthread_mutex_lock(&log_mutex); + for (int i = 0; i < log_count; i++) { + int idx = (log_head - log_count + i + MAX_LOG_LINES) % MAX_LOG_LINES; + char escaped[MAX_LOG_LINE_LEN * 2]; + pldmgr_json_escape(log_buffer[idx], escaped, sizeof(escaped)); + bpos += snprintf(buf + bpos, buf_size - bpos, + "\"%s\"%s", escaped, + (i == log_count - 1) ? "" : ","); + } + pthread_mutex_unlock(&log_mutex); + + bpos += snprintf(buf + bpos, buf_size - bpos, "]}"); + return bpos; +} diff --git a/src/main.c b/src/main.c index 45a200d..8fbff17 100644 --- a/src/main.c +++ b/src/main.c @@ -3,1369 +3,73 @@ * * This is a native PS5 ELF daemon that hosts a web server * to manage payloads and system settings. + * + * All HTTP routing lives in http_server.c. + * This file handles: process init, signal setup, MHD lifecycle, and watchdog. */ -#include #include #include #include #include #include #include -#include #include #include -#include #include -#include "assets_index_html.h" -#include "assets_cache_appcache.h" -#include "assets_favicon_svg.h" -#include "assets_icon_png.h" - #include "pldmgr.h" - - -#include "payload_mgr.h" +#include "config.h" +#include "http_server.h" +#include "repository.h" #include "ps5_launcher.h" #include "app_installer.h" -#include -#include -#include -#define MAX_LOG_LINES 100 -#define MAX_LOG_LINE_LEN 256 -#define RESPONSE_BUFFER_SIZE 65536 -#define CORS_ORIGIN "*" +#define DEFAULT_PORT MENU_PORT -static char log_buffer[MAX_LOG_LINES][MAX_LOG_LINE_LEN]; -static int log_head = 0; -static int log_count = 0; -static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER; -static pthread_cond_t log_cond = PTHREAD_COND_INITIALIZER; -static int log_updated = 0; +/* Defined in http_server.c — set to 0 by /shutdown route */ +extern volatile int http_keep_running; static volatile sig_atomic_t resume_flag = 0; -void handle_sigcont(int sig) { resume_flag = 1; } +static void handle_sigcont(int sig) { resume_flag = 1; } -static pid_t -find_pid(const char* name) { - int mib[4] = {1, 14, 8, 0}; - pid_t mypid = getpid(); - pid_t pid = -1; - size_t buf_size; - uint8_t *buf; +static pid_t find_pid(const char *name) { + int mib[4] = {1, 14, 8, 0}; + pid_t mypid = getpid(); + pid_t pid = -1; + size_t buf_size; + uint8_t *buf; - if(sysctl(mib, 4, 0, &buf_size, 0, 0)) { - pldmgr_log("[PLDMGR] sysctl failed\n"); - return -1; - } + if (sysctl(mib, 4, 0, &buf_size, 0, 0)) { + pldmgr_log("[PLDMGR] sysctl failed\n"); + return -1; + } - if(!(buf=malloc(buf_size))) { - pldmgr_log("[PLDMGR] malloc failed\n"); - return -1; - } + if (!(buf = malloc(buf_size))) { + pldmgr_log("[PLDMGR] malloc failed\n"); + return -1; + } + + if (sysctl(mib, 4, buf, &buf_size, 0, 0)) { + pldmgr_log("[PLDMGR] sysctl failed\n"); + free(buf); + return -1; + } + + for (uint8_t *ptr = buf; ptr < (buf + buf_size);) { + int ki_structsize = *(int *)ptr; + pid_t ki_pid = *(pid_t *)&ptr[72]; + char *ki_tdname = (char *)&ptr[447]; + + ptr += ki_structsize; + if (!strcmp(name, ki_tdname) && ki_pid != mypid) { + pid = ki_pid; + } + } - if(sysctl(mib, 4, buf, &buf_size, 0, 0)) { - pldmgr_log("[PLDMGR] sysctl failed\n"); free(buf); - return -1; - } - - for(uint8_t *ptr=buf; ptr<(buf+buf_size);) { - int ki_structsize = *(int*)ptr; - pid_t ki_pid = *(pid_t*)&ptr[72]; - char *ki_tdname = (char*)&ptr[447]; - - ptr += ki_structsize; - if(!strcmp(name, ki_tdname) && ki_pid != mypid) { - pid = ki_pid; - } - } - - free(buf); - - return pid; -} - -void pldmgr_log(const char *fmt, ...) { - char line[MAX_LOG_LINE_LEN]; - va_list args; - va_start(args, fmt); - vsnprintf(line, sizeof(line), fmt, args); - va_end(args); - - /* Print to stdout */ - printf("%s", line); - - /* Remove trailing newline for internal storage if present */ - size_t len = strlen(line); - while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) { - line[len - 1] = '\0'; - len--; - } - - if (len == 0) - return; - - /* Add to circular buffer with lock */ - pthread_mutex_lock(&log_mutex); - strncpy(log_buffer[log_head], line, MAX_LOG_LINE_LEN); - log_head = (log_head + 1) % MAX_LOG_LINES; - if (log_count < MAX_LOG_LINES) - log_count++; - - /* Signal SSE clients */ - log_updated++; - pthread_cond_broadcast(&log_cond); - pthread_mutex_unlock(&log_mutex); -} -/* SSE Log Callback */ -struct LogSSEConnection { - int last_log_version; - int sent_initial; -}; - -static ssize_t log_stream_callback(void *cls, uint64_t pos, char *buf, - size_t max) { - struct LogSSEConnection *conn = (struct LogSSEConnection *)cls; - - pthread_mutex_lock(&log_mutex); - - /* Initial catch-up */ - if (!conn->sent_initial) { - char initial_batch[MAX_LOG_LINES * (MAX_LOG_LINE_LEN + 16)]; - size_t offset = 0; - - for (int i = 0; i < log_count; i++) { - int idx = (log_head - log_count + i + MAX_LOG_LINES) % MAX_LOG_LINES; - offset += snprintf(initial_batch + offset, sizeof(initial_batch) - offset, - "data: %s\n\n", log_buffer[idx]); - } - - conn->sent_initial = 1; - conn->last_log_version = log_updated; - - if (offset > 0) { - size_t to_copy = (offset < max) ? offset : max; - memcpy(buf, initial_batch, to_copy); - pthread_mutex_unlock(&log_mutex); - return to_copy; - } - } - - /* Wait for new logs */ - while (conn->last_log_version == log_updated) { - struct timespec ts; - clock_gettime(CLOCK_REALTIME, &ts); - ts.tv_sec += 5; /* Keep-alive ping interval */ - - if (pthread_cond_timedwait(&log_cond, &log_mutex, &ts) == ETIMEDOUT) { - /* Send a comment keep-alive */ - const char *ping = ": ping\n\n"; - memcpy(buf, ping, strlen(ping)); - pthread_mutex_unlock(&log_mutex); - return strlen(ping); - } - } - - /* Send new log */ - int idx = (log_head - 1 + MAX_LOG_LINES) % MAX_LOG_LINES; - size_t len = snprintf(buf, max, "data: %s\n\n", log_buffer[idx]); - conn->last_log_version = log_updated; - - pthread_mutex_unlock(&log_mutex); - return len; -} - -static void log_stream_cleanup(void *cls) { free(cls); } -#define DEFAULT_PORT MENU_PORT - -static volatile int server_active_flag = 0; - -int pldmgr_server_is_active() { return server_active_flag; } - -static volatile int keep_running = 1; - -static void add_cors_headers(struct MHD_Response *resp) { - MHD_add_response_header(resp, "Access-Control-Allow-Origin", CORS_ORIGIN); -} - -/* - * Allocate a per-request response buffer. - * On success: sets *out to the buffer and returns NULL. - * On failure: returns a ready-to-queue 500 JSON error response (caller must - * queue and destroy it, then early-return). - */ -static struct MHD_Response *alloc_response_buffer(char **out) { - *out = malloc(RESPONSE_BUFFER_SIZE); - if (*out) - return NULL; - static const char oom[] = "{\"error\":\"Out of memory\"}"; - struct MHD_Response *resp = MHD_create_response_from_buffer( - sizeof(oom) - 1, (void *)oom, MHD_RESPMEM_PERSISTENT); - MHD_add_response_header(resp, "Content-Type", "application/json"); - add_cors_headers(resp); - return resp; -} - -static int has_supported_extension(const char *path) { - const char *ext = strrchr(path, '.'); - if (!ext) - return 0; - return strcasecmp(ext, ".elf") == 0 || strcasecmp(ext, ".bin") == 0; -} - -static int is_safe_filename(const char *name) { - if (!name || name[0] == '\0') - return 0; - if (strstr(name, "/") || strstr(name, "..")) - return 0; - return 1; -} - -static int is_allowed_payload_path(const char *path) { - if (!path || path[0] == '\0') - return 0; - if (strstr(path, "..")) - return 0; - if (!has_supported_extension(path)) - return 0; - - for (int i = 0; i < SCAN_DIRS_COUNT; i++) { - size_t len = strlen(SCAN_DIRS[i]); - if (strncmp(path, SCAN_DIRS[i], len) == 0 && - (path[len] == '/' || path[len] == '\0')) { - return 1; - } - } - - /* Allow USB root payloads if SCAN_USB_PAYLOADS is enabled in config */ - if (strncmp(path, "/mnt/usb", 8) == 0 && - path[8] >= '0' && path[8] <= '7' && - path[9] == '/') { - if (pldmgr_read_config_bool("SCAN_USB_PAYLOADS", 0)) { - return 1; - } - } - - return 0; -} - -/* State for POST requests */ -struct PostStatus { - char *data; - size_t size; - int error; -}; - -/* State for file uploads */ -struct UploadStatus { - FILE *fp; - size_t total_size; - int error; - char filename[256]; - char temp_path[512]; - char repo_url[512]; -}; - -static void read_next_config_values(int *enabled, long *repo_update, - int *browser_open, int *auto_delay, - int *kill_disc_player, int *scan_usb, int *auto_install_app, - int *multi_sources) { - FILE *f = fopen(PLDMGR_CONFIG_PATH, "r"); - char line[256]; - - *enabled = 0; - *repo_update = 0; - *browser_open = 1; /* Default on */ - *auto_delay = 5; /* Default 5s */ - *kill_disc_player = 1; /* Default on */ - *scan_usb = 0; /* Default off */ - *auto_install_app = 1; /* Default on */ - *multi_sources = 0; /* Default off */ - - if (!f) { - return; - } - - while (fgets(line, sizeof(line), f)) { - if (strncmp(line, "AUTOLOAD_ENABLED=", 17) == 0) { - *enabled = atoi(line + 17); - } else if (strncmp(line, "LAST_REPOSITORY_UPDATE=", 23) == 0) { - *repo_update = atol(line + 23); - } else if (strncmp(line, "AUTO_BROWSER_OPEN=", 18) == 0) { - *browser_open = atoi(line + 18); - } else if (strncmp(line, "AUTOLOAD_DELAY=", 15) == 0) { - *auto_delay = atoi(line + 15); - } else if (strncmp(line, "KILL_DISC_PLAYER_ON_STARTUP=", 28) == 0) { - *kill_disc_player = atoi(line + 28); - } else if (strncmp(line, "SCAN_USB_PAYLOADS=", 18) == 0) { - *scan_usb = atoi(line + 18); - } else if (strncmp(line, "AUTO_INSTALL_APP=", 17) == 0) { - *auto_install_app = atoi(line + 17); - } else if (strncmp(line, "MULTI_SOURCES_ENABLED=", 22) == 0) { - *multi_sources = atoi(line + 22); - } - } - fclose(f); -} - -static int write_next_config_values(int enabled, long repo_update, - int browser_open, int auto_delay, - int kill_disc_player, int scan_usb, int auto_install_app, - int multi_sources) { - FILE *f; - - mkdir(BASE_DATA_DIR, 0777); - f = fopen(PLDMGR_CONFIG_PATH, "w"); - if (!f) { - return -1; - } - - fprintf(f, "AUTOLOAD_ENABLED=%d\n", enabled ? 1 : 0); - fprintf(f, "LAST_REPOSITORY_UPDATE=%ld\n", repo_update); - fprintf(f, "AUTO_BROWSER_OPEN=%d\n", browser_open ? 1 : 0); - fprintf(f, "AUTOLOAD_DELAY=%d\n", auto_delay); - fprintf(f, "KILL_DISC_PLAYER_ON_STARTUP=%d\n", kill_disc_player ? 1 : 0); - fprintf(f, "SCAN_USB_PAYLOADS=%d\n", scan_usb ? 1 : 0); - fprintf(f, "AUTO_INSTALL_APP=%d\n", auto_install_app ? 1 : 0); - fprintf(f, "MULTI_SOURCES_ENABLED=%d\n", multi_sources ? 1 : 0); - fclose(f); - return 0; -} - -/* Callback for handling HTTP requests */ -static enum MHD_Result on_request(void *cls, struct MHD_Connection *conn, - const char *url, const char *method, - const char *version, const char *upload_data, - size_t *upload_data_size, void **con_cls) { - - /* Handle CORS Preflight (OPTIONS) */ - if (strcmp(method, "OPTIONS") == 0) { - struct MHD_Response *resp = - MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT); - add_cors_headers(resp); - MHD_add_response_header(resp, "Access-Control-Allow-Methods", - "GET, POST, OPTIONS"); - MHD_add_response_header(resp, "Access-Control-Allow-Headers", - "Content-Type"); - enum MHD_Result ret = MHD_queue_response(conn, MHD_HTTP_OK, resp); - MHD_destroy_response(resp); - return ret; - } - - /* Set flag that we received a request (meaning browser is open) */ - server_active_flag = 1; - - /* Initial call for a new request */ - if (*con_cls == NULL) { - if (strncmp(url, ROUTE_UPLOAD, strlen(ROUTE_UPLOAD)) == 0) { - struct UploadStatus *status = malloc(sizeof(struct UploadStatus)); - status->fp = NULL; - status->total_size = 0; - status->error = 0; - - const char *filename = - MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "filename"); - if (filename && !strstr(filename, "/") && !strstr(filename, "..")) { - char path[512]; - mkdir(BASE_DATA_DIR, 0777); - mkdir(PAYLOADS_STORAGE_DIR, 0777); - snprintf(path, sizeof(path), "%s/%s.tmp", PAYLOADS_STORAGE_DIR, filename); - - strncpy(status->filename, filename, sizeof(status->filename) - 1); - status->filename[sizeof(status->filename) - 1] = '\0'; - strncpy(status->temp_path, path, sizeof(status->temp_path) - 1); - status->temp_path[sizeof(status->temp_path) - 1] = '\0'; - - status->fp = fopen(path, "wb"); - if (!status->fp) { - pldmgr_log("[PLDMGR] !!! FAILED to open temp file: %s\n", path); - status->error = 1; - } else { - pldmgr_log("[PLDMGR] Starting upload to temp: %s\n", path); - } - } else { - pldmgr_log("[PLDMGR] !!! Upload failed: Missing filename parameter\n"); - status->error = 1; - } - *con_cls = status; - return MHD_YES; - } - - if (strcmp(url, ROUTE_SET_CONFIG) == 0 && strcmp(method, "POST") == 0) { - struct PostStatus *status = malloc(sizeof(struct PostStatus)); - status->data = NULL; - status->size = 0; - status->error = 0; - *con_cls = status; - return MHD_YES; - } - - if (strcmp(url, ROUTE_REPO_PUSH) == 0 && strcmp(method, "POST") == 0) { - struct PostStatus *status = malloc(sizeof(struct PostStatus)); - status->data = NULL; - status->size = 0; - status->error = 0; - *con_cls = status; - return MHD_YES; - } - - if (strcmp(url, ROUTE_SOURCES_SET) == 0 && strcmp(method, "POST") == 0) { - struct PostStatus *status = malloc(sizeof(struct PostStatus)); - status->data = NULL; - status->size = 0; - status->error = 0; - *con_cls = status; - return MHD_YES; - } - - if (strncmp(url, ROUTE_REPO_INSTALL_PUSH, - strlen(ROUTE_REPO_INSTALL_PUSH)) == 0 && - strcmp(method, "POST") == 0) { - struct UploadStatus *status = malloc(sizeof(struct UploadStatus)); - status->fp = NULL; - status->total_size = 0; - status->error = 0; - const char *filename = - MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "filename"); - if (filename && !strstr(filename, "/") && !strstr(filename, "..")) { - char path[512]; - mkdir(BASE_DATA_DIR, 0777); - mkdir(PAYLOADS_STORAGE_DIR, 0777); - snprintf(path, sizeof(path), "%s/%s.part", PAYLOADS_STORAGE_DIR, - filename); - strncpy(status->filename, filename, sizeof(status->filename) - 1); - status->filename[sizeof(status->filename) - 1] = '\0'; - strncpy(status->temp_path, path, sizeof(status->temp_path) - 1); - status->temp_path[sizeof(status->temp_path) - 1] = '\0'; - - const char *repo_url = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "repo_url"); - if (repo_url) { - strncpy(status->repo_url, repo_url, sizeof(status->repo_url) - 1); - status->repo_url[sizeof(status->repo_url) - 1] = '\0'; - } else { - strncpy(status->repo_url, REPOSITORY_SOURCE_URL, sizeof(status->repo_url) - 1); - status->repo_url[sizeof(status->repo_url) - 1] = '\0'; - } - - status->fp = fopen(path, "wb"); - if (!status->fp) { - pldmgr_log("[PLDMGR] !!! Failed to open for install: %s\n", path); - status->error = 1; - } else { - pldmgr_log("[PLDMGR] Installing payload: %s\n", path); - } - } else { - pldmgr_log("[PLDMGR] !!! install_push: invalid filename\n"); - status->error = 1; - } - *con_cls = status; - return MHD_YES; - } - - *con_cls = (void *)1; - return MHD_YES; - } - - /* Handle POST data for set_config */ - if (strcmp(url, ROUTE_SET_CONFIG) == 0 && strcmp(method, "POST") == 0) { - struct PostStatus *status = (struct PostStatus *)*con_cls; - if (*upload_data_size != 0) { - char *new_data = - realloc(status->data, status->size + *upload_data_size + 1); - if (!new_data) { - status->error = 1; - } else { - status->data = new_data; - memcpy(status->data + status->size, upload_data, *upload_data_size); - status->size += *upload_data_size; - status->data[status->size] = '\0'; - } - *upload_data_size = 0; - return MHD_YES; - } else { - /* Finished receiving JSON */ - pldmgr_log("[PLDMGR] Received config update: %s\n", - status->data ? status->data : "(null)"); - - /* Very basic JSON extraction for "AUTOLOAD_LIST":"..." and - * "AUTOLOAD_ENABLED":true/false */ - if (status->data && !status->error) { - int enabled = -1; - char *enabled_pos = strstr(status->data, "\"AUTOLOAD_ENABLED\""); - if (enabled_pos) { - char *val = strchr(enabled_pos, ':'); - if (val) { - val++; - while (*val == ' ') - val++; - if (strncmp(val, "true", 4) == 0) - enabled = 1; - else if (strncmp(val, "false", 5) == 0) - enabled = 0; - } - } - - if (enabled != -1) { - int ex_en, ex_br, ex_del, ex_kill, ex_usb, ex_install, ex_multi; - long ex_repo; - read_next_config_values(&ex_en, &ex_repo, &ex_br, &ex_del, &ex_kill, &ex_usb, &ex_install, &ex_multi); - - /* Update individual fields from JSON if present */ - int browser_open = ex_br; - char *browser_pos = strstr(status->data, "\"AUTO_BROWSER_OPEN\""); - if (browser_pos) { - char *val = strchr(browser_pos, ':'); - if (val) { - val++; - while (*val == ' ') - val++; - if (strncmp(val, "true", 4) == 0) - browser_open = 1; - else if (strncmp(val, "false", 5) == 0) - browser_open = 0; - } - } - - int auto_delay = ex_del; - char *delay_pos = strstr(status->data, "\"AUTOLOAD_DELAY\""); - if (delay_pos) { - char *val = strchr(delay_pos, ':'); - if (val) { - val++; - while (*val == ' ') - val++; - auto_delay = atoi(val); - } - } - - int kill_disc = ex_kill; - char *kill_pos = - strstr(status->data, "\"KILL_DISC_PLAYER_ON_STARTUP\""); - if (kill_pos) { - char *val = strchr(kill_pos, ':'); - if (val) { - val++; - while (*val == ' ') - val++; - if (strncmp(val, "true", 4) == 0) - kill_disc = 1; - else if (strncmp(val, "false", 5) == 0) - kill_disc = 0; - } - } - - int scan_usb = ex_usb; - char *usb_pos = strstr(status->data, "\"SCAN_USB_PAYLOADS\""); - if (usb_pos) { - char *val = strchr(usb_pos, ':'); - if (val) { - val++; - while (*val == ' ') - val++; - if (strncmp(val, "true", 4) == 0) - scan_usb = 1; - else if (strncmp(val, "false", 5) == 0) - scan_usb = 0; - } - } - - int auto_install = ex_install; - char *install_pos = strstr(status->data, "\"AUTO_INSTALL_APP\""); - if (install_pos) { - char *val = strchr(install_pos, ':'); - if (val) { - val++; - while (*val == ' ') - val++; - if (strncmp(val, "true", 4) == 0) - auto_install = 1; - else if (strncmp(val, "false", 5) == 0) - auto_install = 0; - } - } - - int multi_sources = ex_multi; - char *multi_pos = strstr(status->data, "\"MULTI_SOURCES_ENABLED\""); - if (multi_pos) { - char *val = strchr(multi_pos, ':'); - if (val) { - val++; - while (*val == ' ') - val++; - if (strncmp(val, "true", 4) == 0) - multi_sources = 1; - else if (strncmp(val, "false", 5) == 0) - multi_sources = 0; - } - } - - if (write_next_config_values(enabled, ex_repo, browser_open, - auto_delay, kill_disc, scan_usb, auto_install, multi_sources) == 0) { - pldmgr_log("[PLDMGR] Saved config to %s\n", PLDMGR_CONFIG_PATH); - } - if (enabled == 0) - pldmgr_autoload_abort(); - } - - char *list_start = strstr(status->data, "\"AUTOLOAD_LIST\""); - if (list_start) { - char *val = strchr(list_start, ':'); - if (val) { - val++; - while (*val == ' ' || *val == '\"') - val++; - - /* Find the end of the string value */ - char *list_end = strchr(val, '\"'); - size_t list_len = list_end ? (size_t)(list_end - val) : 0; - char *list_val = malloc(list_len + 1); - if (list_val) { - memcpy(list_val, val, list_len); - list_val[list_len] = '\0'; - - mkdir(BASE_DATA_DIR, 0777); - FILE *f = fopen(AUTOLOAD_CONFIG_PATH, "w"); - if (f) { - char *token = strtok(list_val, ","); - while (token) { - fprintf(f, "%s\n", token); - token = strtok(NULL, ","); - } - fclose(f); - pldmgr_log("[PLDMGR] Saved autoload list to %s\n", - AUTOLOAD_CONFIG_PATH); - } - free(list_val); - } - } - } - } - - if (status->data) - free(status->data); - free(status); - *con_cls = NULL; - - struct MHD_Response *resp = MHD_create_response_from_buffer( - strlen(MSG_OK), (void *)MSG_OK, MHD_RESPMEM_MUST_COPY); - add_cors_headers(resp); - enum MHD_Result ret = MHD_queue_response(conn, MHD_HTTP_OK, resp); - MHD_destroy_response(resp); - return ret; - } - } - - /* Handle POST data for /repository_push (browser fetched JSON) */ - if (strcmp(url, ROUTE_REPO_PUSH) == 0 && strcmp(method, "POST") == 0) { - struct PostStatus *status = (struct PostStatus *)*con_cls; - if (*upload_data_size != 0) { - char *nd = realloc(status->data, status->size + *upload_data_size + 1); - if (!nd) { - status->error = 1; - } else { - status->data = nd; - memcpy(status->data + status->size, upload_data, *upload_data_size); - status->size += *upload_data_size; - status->data[status->size] = '\0'; - } - *upload_data_size = 0; - return MHD_YES; - } else { - int ok = -1; - if (status->data && !status->error) - ok = payload_mgr_repository_push_json(status->data, status->size); - if (status->data) - free(status->data); - free(status); - *con_cls = NULL; - - /* Return the current list JSON so the frontend refreshes in one trip */ - char *resp_buf; - struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf); - if (oom_resp) { - enum MHD_Result ret2 = MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp); - MHD_destroy_response(oom_resp); - return ret2; - } - - size_t len = payload_mgr_repository_list_json(resp_buf, - RESPONSE_BUFFER_SIZE, 0); - struct MHD_Response *resp = MHD_create_response_from_buffer( - len, (void *)resp_buf, MHD_RESPMEM_MUST_FREE); - MHD_add_response_header(resp, "Content-Type", "application/json"); - add_cors_headers(resp); - enum MHD_Result ret2 = MHD_queue_response( - conn, ok == 0 ? MHD_HTTP_OK : MHD_HTTP_BAD_REQUEST, resp); - MHD_destroy_response(resp); - return ret2; - } - } - - /* Handle POST data for /sources_set */ - if (strcmp(url, ROUTE_SOURCES_SET) == 0 && strcmp(method, "POST") == 0) { - struct PostStatus *status = (struct PostStatus *)*con_cls; - if (*upload_data_size != 0) { - char *nd = realloc(status->data, status->size + *upload_data_size + 1); - if (!nd) { - status->error = 1; - } else { - status->data = nd; - memcpy(status->data + status->size, upload_data, *upload_data_size); - status->size += *upload_data_size; - status->data[status->size] = '\0'; - } - *upload_data_size = 0; - return MHD_YES; - } else { - int ok = -1; - if (status->data && !status->error) - ok = payload_mgr_sources_save(status->data, status->size); - if (status->data) - free(status->data); - free(status); - *con_cls = NULL; - - const char *msg = ok == 0 ? MSG_OK : "Failed to save sources"; - struct MHD_Response *resp_ss = MHD_create_response_from_buffer( - strlen(msg), (void *)msg, MHD_RESPMEM_MUST_COPY); - add_cors_headers(resp_ss); - enum MHD_Result ret_ss = MHD_queue_response( - conn, ok == 0 ? MHD_HTTP_OK : MHD_HTTP_INTERNAL_SERVER_ERROR, resp_ss); - MHD_destroy_response(resp_ss); - return ret_ss; - } - } - - /* Chunked data arrival */ - if (strncmp(url, ROUTE_UPLOAD, strlen(ROUTE_UPLOAD)) == 0) { - struct UploadStatus *status = (struct UploadStatus *)*con_cls; - if (*upload_data_size != 0) { - if (status->fp && !status->error) { - size_t written = fwrite(upload_data, 1, *upload_data_size, status->fp); - if (written != *upload_data_size) { - pldmgr_log("[PLDMGR] !!! Write error: expected %zu, got %zu\n", - *upload_data_size, written); - status->error = 1; - } - status->total_size += written; - } - *upload_data_size = 0; - return MHD_YES; - } else { - /* Upload finished */ - if (status->fp) { - fflush(status->fp); - fclose(status->fp); - status->fp = NULL; - - if (!status->error) { - char msg[256]; - if (payload_mgr_import_to_storage(status->filename, status->temp_path, "web_upload", "", msg, sizeof(msg)) != 0) { - pldmgr_log("[PLDMGR] !!! Failed to commit upload: %s\n", msg); - status->error = 1; - } - } - } - pldmgr_log("[PLDMGR] Upload finished. Total bytes: %zu, Error: %d\n", - status->total_size, status->error); - - int err = status->error; - free(status); - *con_cls = NULL; - - const char *msg = err ? "Error during upload\n" : MSG_OK; - struct MHD_Response *resp = MHD_create_response_from_buffer( - strlen(msg), (void *)msg, MHD_RESPMEM_MUST_COPY); - add_cors_headers(resp); - enum MHD_Result ret = MHD_queue_response( - conn, err ? MHD_HTTP_INTERNAL_SERVER_ERROR : MHD_HTTP_OK, resp); - MHD_destroy_response(resp); - return ret; - } - } - - /* Chunked data arrival for /repository_install_push */ - if (strncmp(url, ROUTE_REPO_INSTALL_PUSH, strlen(ROUTE_REPO_INSTALL_PUSH)) == - 0) { - struct UploadStatus *status = (struct UploadStatus *)*con_cls; - if (*upload_data_size != 0) { - if (status->fp && !status->error) { - size_t written = fwrite(upload_data, 1, *upload_data_size, status->fp); - if (written != *upload_data_size) { - status->error = 1; - } - status->total_size += written; - } - *upload_data_size = 0; - return MHD_YES; - } else { - if (status->fp) { - fflush(status->fp); - fclose(status->fp); - } - int err = status->error; - - char msg_buf[1024] = ""; - if (!err) { - /* Commit the install: verify SHA256 and move to final destination */ - if (payload_mgr_repository_install_commit( - status->filename, status->temp_path, "repository", status->repo_url, msg_buf, - sizeof(msg_buf)) != 0) { - err = 1; - } - } - - pldmgr_log("[PLDMGR] Payload install %s (%zu bytes)\n", - err ? "FAILED" : "complete", status->total_size); - free(status); - *con_cls = NULL; - if (err && msg_buf[0] == '\0') { - snprintf(msg_buf, sizeof(msg_buf), "Write error"); - } - char json_resp[1024]; - snprintf(json_resp, sizeof(json_resp), "{\"ok\":%s,\"message\":\"%s\"}", - err ? "false" : "true", err ? msg_buf : "Installed"); - struct MHD_Response *resp2 = MHD_create_response_from_buffer( - strlen(json_resp), (void *)json_resp, MHD_RESPMEM_MUST_COPY); - MHD_add_response_header(resp2, "Content-Type", "application/json"); - add_cors_headers(resp2); - enum MHD_Result ret2 = MHD_queue_response( - conn, err ? MHD_HTTP_INTERNAL_SERVER_ERROR : MHD_HTTP_OK, resp2); - MHD_destroy_response(resp2); - return ret2; - } - } - - /* Only log significant requests, not pollers like /log or static assets */ - if (strcmp(url, ROUTE_LOG) != 0 && strcmp(url, ROUTE_INDEX) != 0 && - strcmp(url, ROUTE_INDEX_HTML) != 0 && strcmp(url, "/favicon.svg") != 0 && - strcmp(url, "/icon.png") != 0) { - pldmgr_log("[PLDMGR] Request: %s %s\n", method, url); - } - - struct MHD_Response *resp = NULL; - enum MHD_Result ret; - int http_status = MHD_HTTP_OK; - - if (strcmp(url, ROUTE_USB_MOVE_CHECK) == 0) { - const char *path = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "path"); - if (path) { - char *resp_buf; - struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf); - if (oom_resp) { - ret = MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp); - MHD_destroy_response(oom_resp); - return ret; - } - int rc = payload_mgr_usb_check(path, resp_buf, RESPONSE_BUFFER_SIZE); - resp = MHD_create_response_from_buffer(strlen(resp_buf), (void *)resp_buf, MHD_RESPMEM_MUST_FREE); - MHD_add_response_header(resp, "Content-Type", "application/json"); - add_cors_headers(resp); - ret = MHD_queue_response(conn, rc == 0 ? MHD_HTTP_OK : MHD_HTTP_BAD_REQUEST, resp); - MHD_destroy_response(resp); - return ret; - } else { - const char *err = "{\"error\":\"Missing path\"}"; - resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY); - MHD_add_response_header(resp, "Content-Type", "application/json"); - } - add_cors_headers(resp); - ret = MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp); - MHD_destroy_response(resp); - return ret; - } - - if (strcmp(url, ROUTE_USB_MOVE_PERFORM) == 0) { - const char *path = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "path"); - const char *overwrite_str = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "overwrite"); - int overwrite = (overwrite_str && strcmp(overwrite_str, "true") == 0) ? 1 : 0; - if (path) { - char *resp_buf; - struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf); - if (oom_resp) { - ret = MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp); - MHD_destroy_response(oom_resp); - return ret; - } - int rc = payload_mgr_usb_move(path, overwrite, resp_buf, RESPONSE_BUFFER_SIZE); - resp = MHD_create_response_from_buffer(strlen(resp_buf), (void *)resp_buf, MHD_RESPMEM_MUST_FREE); - MHD_add_response_header(resp, "Content-Type", "application/json"); - add_cors_headers(resp); - ret = MHD_queue_response(conn, rc == 0 ? MHD_HTTP_OK : MHD_HTTP_BAD_REQUEST, resp); - MHD_destroy_response(resp); - return ret; - } else { - const char *err = "{\"error\":\"Missing path\"}"; - resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY); - MHD_add_response_header(resp, "Content-Type", "application/json"); - } - add_cors_headers(resp); - ret = MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp); - MHD_destroy_response(resp); - return ret; - } - - /* Route: Index or index.html */ - if (strcmp(url, ROUTE_INDEX) == 0 || strcmp(url, ROUTE_INDEX_HTML) == 0) { - resp = MHD_create_response_from_buffer(assets_index_html_len, - (void *)assets_index_html, - MHD_RESPMEM_PERSISTENT); - MHD_add_response_header(resp, "Content-Type", "text/html"); - MHD_add_response_header(resp, "Cache-Control", "no-cache, must-revalidate"); - } else if (strcmp(url, ROUTE_CACHE_MANIFEST) == 0) { - - resp = MHD_create_response_from_buffer(assets_cache_appcache_len, - (void *)assets_cache_appcache, - MHD_RESPMEM_PERSISTENT); - MHD_add_response_header(resp, "Content-Type", "text/cache-manifest"); - MHD_add_response_header(resp, "Cache-Control", "no-cache, must-revalidate"); - } else if (strcmp(url, "/favicon.svg") == 0) { - resp = MHD_create_response_from_buffer(assets_favicon_svg_len, - (void *)assets_favicon_svg, - MHD_RESPMEM_PERSISTENT); - MHD_add_response_header(resp, "Content-Type", "image/svg+xml"); - MHD_add_response_header(resp, "Cache-Control", "max-age=604800"); - } else if (strcmp(url, "/icon.png") == 0) { - resp = MHD_create_response_from_buffer(assets_icon_png_len, - (void *)assets_icon_png, - MHD_RESPMEM_PERSISTENT); - MHD_add_response_header(resp, "Content-Type", "image/png"); - MHD_add_response_header(resp, "Cache-Control", "max-age=604800"); - } else if (strcmp(url, ROUTE_CHECK) == 0) { - - - const char *filename = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "filename"); - if (!filename) { - const char *err = "{\"error\":\"Missing filename\"}"; - resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY); - MHD_add_response_header(resp, "Content-Type", "application/json"); - add_cors_headers(resp); - return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp); - } - if (!is_safe_filename(filename)) { - const char *err = "{\"error\":\"Invalid filename\"}"; - resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY); - MHD_add_response_header(resp, "Content-Type", "application/json"); - add_cors_headers(resp); - return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp); - } - char *resp_buf; - struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf); - if (oom_resp) - return MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp); - payload_mgr_check_existing(filename, resp_buf, RESPONSE_BUFFER_SIZE); - resp = MHD_create_response_from_buffer(strlen(resp_buf), (void *)resp_buf, MHD_RESPMEM_MUST_FREE); - MHD_add_response_header(resp, "Content-Type", "application/json"); - add_cors_headers(resp); - return MHD_queue_response(conn, MHD_HTTP_OK, resp); - } else if (strcmp(url, ROUTE_LIST_PAYLOADS) == 0) { - char *resp_buf; - struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf); - if (oom_resp) - return MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp); - size_t len = payload_mgr_list_json(resp_buf, RESPONSE_BUFFER_SIZE); - resp = MHD_create_response_from_buffer(len, (void *)resp_buf, - MHD_RESPMEM_MUST_FREE); - MHD_add_response_header(resp, "Content-Type", "application/json"); - } else if (strcmp(url, ROUTE_PROCESSES_LIST) == 0) { - char *resp_buf; - struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf); - if (oom_resp) - return MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp); - size_t len = payload_mgr_process_list_json(resp_buf, RESPONSE_BUFFER_SIZE); - resp = MHD_create_response_from_buffer(len, (void *)resp_buf, - MHD_RESPMEM_MUST_FREE); - MHD_add_response_header(resp, "Content-Type", "application/json"); - } else if (strcmp(url, ROUTE_PROCESS_KILL) == 0) { - const char *pid_str = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "pid"); - if (!pid_str) { - const char *err = "{\"ok\":false,\"message\":\"Missing pid\"}"; - resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY); - MHD_add_response_header(resp, "Content-Type", "application/json"); - add_cors_headers(resp); - return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp); - } - int pid = atoi(pid_str); - int rc = payload_mgr_process_kill(pid); - char json_resp[256]; - snprintf(json_resp, sizeof(json_resp), "{\"ok\":%s,\"message\":\"%s\"}", - rc == 0 ? "true" : "false", rc == 0 ? "Killed" : "Failed to kill"); - resp = MHD_create_response_from_buffer(strlen(json_resp), (void *)json_resp, MHD_RESPMEM_MUST_COPY); - MHD_add_response_header(resp, "Content-Type", "application/json"); - add_cors_headers(resp); - return MHD_queue_response(conn, rc == 0 ? MHD_HTTP_OK : MHD_HTTP_INTERNAL_SERVER_ERROR, resp); - } else if (strcmp(url, ROUTE_REPO_LIST) == 0) { - char *resp_buf; - struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf); - if (oom_resp) - return MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp); - int multi_en = 0, _a, _b, _c, _d, _e, _f; - long _g; - read_next_config_values(&_a, &_g, &_b, &_c, &_d, &_e, &_f, &multi_en); - size_t len; - if (multi_en) - len = payload_mgr_multi_repository_list_json(resp_buf, RESPONSE_BUFFER_SIZE, 0); - else - len = payload_mgr_repository_list_json(resp_buf, RESPONSE_BUFFER_SIZE, 0); - resp = MHD_create_response_from_buffer(len, (void *)resp_buf, - MHD_RESPMEM_MUST_FREE); - MHD_add_response_header(resp, "Content-Type", "application/json"); - } else if (strcmp(url, ROUTE_REPO_REFRESH) == 0) { - char *resp_buf; - struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf); - if (oom_resp) - return MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp); - int multi_en2 = 0, _a2, _b2, _c2, _d2, _e2, _f2; - long _g2; - read_next_config_values(&_a2, &_g2, &_b2, &_c2, &_d2, &_e2, &_f2, &multi_en2); - size_t len2; - if (multi_en2) - len2 = payload_mgr_multi_repository_list_json(resp_buf, RESPONSE_BUFFER_SIZE, 1); - else - len2 = payload_mgr_repository_list_json(resp_buf, RESPONSE_BUFFER_SIZE, 1); - resp = MHD_create_response_from_buffer(len2, (void *)resp_buf, - MHD_RESPMEM_MUST_FREE); - MHD_add_response_header(resp, "Content-Type", "application/json"); - } else if (strcmp(url, ROUTE_REPO_INSTALL) == 0) { - const char *filename = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "filename"); - const char *repo_url = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "repo_url"); - const char *source_id = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "source_id"); - if (!filename) { - const char *err = "{\"ok\":false,\"message\":\"Missing filename\"}"; - resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY); - MHD_add_response_header(resp, "Content-Type", "application/json"); - add_cors_headers(resp); - return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp); - } - if (!is_safe_filename(filename)) { - const char *err = "{\"ok\":false,\"message\":\"Invalid filename\"}"; - resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY); - MHD_add_response_header(resp, "Content-Type", "application/json"); - add_cors_headers(resp); - return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp); - } - - char msg_buf[1024] = ""; - int rc; - /* Use multi-source install if source_id is provided */ - if (source_id && source_id[0]) { - rc = payload_mgr_multi_repository_install(filename, source_id, - repo_url ? repo_url : "", - msg_buf, sizeof(msg_buf)); - } else { - const char *detail = (repo_url && repo_url[0]) ? repo_url : REPOSITORY_SOURCE_URL; - rc = payload_mgr_repository_install_download(filename, detail, msg_buf, sizeof(msg_buf)); - } - if (msg_buf[0] == '\0') { - snprintf(msg_buf, sizeof(msg_buf), rc == 0 ? "Installed" : "Install failed"); - } - - char json_resp[1024]; - snprintf(json_resp, sizeof(json_resp), "{\"ok\":%s,\"message\":\"%s\"}", - rc == 0 ? "true" : "false", msg_buf); - resp = MHD_create_response_from_buffer(strlen(json_resp), (void *)json_resp, MHD_RESPMEM_MUST_COPY); - MHD_add_response_header(resp, "Content-Type", "application/json"); - add_cors_headers(resp); - return MHD_queue_response(conn, rc == 0 ? MHD_HTTP_OK : MHD_HTTP_INTERNAL_SERVER_ERROR, resp); - } else if (strcmp(url, ROUTE_SOURCES_LIST) == 0) { - char *resp_buf; - struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf); - if (oom_resp) - return MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp); - payload_mgr_sources_list_json(resp_buf, RESPONSE_BUFFER_SIZE); - resp = MHD_create_response_from_buffer(strlen(resp_buf), (void *)resp_buf, MHD_RESPMEM_MUST_FREE); - MHD_add_response_header(resp, "Content-Type", "application/json"); - } else if (strcmp(url, ROUTE_SOURCES_ADD) == 0) { - const char *src_url = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "url"); - if (!src_url || !src_url[0]) { - const char *err = "{\"ok\":false,\"message\":\"Missing url parameter\"}"; - resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY); - MHD_add_response_header(resp, "Content-Type", "application/json"); - add_cors_headers(resp); - return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp); - } - char msg_buf_src[512] = ""; - int rc_src = payload_mgr_sources_add(src_url, msg_buf_src, sizeof(msg_buf_src)); - char json_resp_src[1024]; - if (rc_src == 0) { - char name_escaped[512]; - pldmgr_json_escape(msg_buf_src, name_escaped, sizeof(name_escaped)); - snprintf(json_resp_src, sizeof(json_resp_src), - "{\"ok\":true,\"name\":\"%s\"}", name_escaped); - } else { - char msg_escaped[512]; - pldmgr_json_escape(msg_buf_src, msg_escaped, sizeof(msg_escaped)); - snprintf(json_resp_src, sizeof(json_resp_src), - "{\"ok\":false,\"message\":\"%s\"}", msg_escaped); - } - resp = MHD_create_response_from_buffer(strlen(json_resp_src), (void *)json_resp_src, MHD_RESPMEM_MUST_COPY); - MHD_add_response_header(resp, "Content-Type", "application/json"); - add_cors_headers(resp); - return MHD_queue_response(conn, rc_src == 0 ? MHD_HTTP_OK : MHD_HTTP_BAD_REQUEST, resp); - } else if (strcmp(url, ROUTE_SOURCES_REMOVE) == 0) { - const char *idx_str = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "index"); - if (!idx_str) { - const char *err = "{\"ok\":false,\"message\":\"Missing index parameter\"}"; - resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY); - MHD_add_response_header(resp, "Content-Type", "application/json"); - add_cors_headers(resp); - return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp); - } - int idx = atoi(idx_str); - char msg_rm[256] = ""; - int rc_rm = payload_mgr_sources_remove(idx, msg_rm, sizeof(msg_rm)); - char json_rm[512]; - char msg_rm_e[256]; - pldmgr_json_escape(msg_rm, msg_rm_e, sizeof(msg_rm_e)); - snprintf(json_rm, sizeof(json_rm), "{\"ok\":%s,\"message\":\"%s\"}", - rc_rm == 0 ? "true" : "false", msg_rm_e); - resp = MHD_create_response_from_buffer(strlen(json_rm), (void *)json_rm, MHD_RESPMEM_MUST_COPY); - MHD_add_response_header(resp, "Content-Type", "application/json"); - add_cors_headers(resp); - return MHD_queue_response(conn, rc_rm == 0 ? MHD_HTTP_OK : MHD_HTTP_BAD_REQUEST, resp); - } else if (strncmp(url, ROUTE_LOAD_PAYLOAD, strlen(ROUTE_LOAD_PAYLOAD)) == - 0) { - const char *path = url + strlen(ROUTE_LOAD_PAYLOAD); - const char *final_path = NULL; - char resolved_path[512]; - if (is_allowed_payload_path(path)) { - final_path = path; - } else { - const char *filename = strrchr(path, '/'); - filename = filename ? filename + 1 : path; - if (is_safe_filename(filename) && - payload_mgr_resolve_path(filename, resolved_path, - sizeof(resolved_path)) == 0) { - final_path = resolved_path; - } - } - - if (!final_path) { - pldmgr_log("[PLDMGR] !!! Payload path rejected: %s\n", path); - const char *err = "Invalid payload name\n"; - resp = MHD_create_response_from_buffer(strlen(err), (void *)err, - MHD_RESPMEM_PERSISTENT); - http_status = MHD_HTTP_BAD_REQUEST; - } else if (ps5_launch_elf(final_path) == 0) { - resp = MHD_create_response_from_buffer(strlen(MSG_OK), (void *)MSG_OK, - MHD_RESPMEM_PERSISTENT); - } else { - const char *err = "Failed to launch payload\n"; - resp = MHD_create_response_from_buffer(strlen(err), (void *)err, - MHD_RESPMEM_PERSISTENT); - http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - } - MHD_add_response_header(resp, "Content-Type", "text/plain"); - } else if (strncmp(url, ROUTE_DELETE, strlen(ROUTE_DELETE)) == 0) { - const char *filename = - MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "filename"); - if (filename) { - /* Basic safety: skip if filename contains / or .. */ - if (strstr(filename, "/") || strstr(filename, "..")) { - const char *err = "Invalid filename\n"; - resp = MHD_create_response_from_buffer(strlen(err), (void *)err, - MHD_RESPMEM_PERSISTENT); - } else { - if (payload_mgr_delete_payload_file(filename) == 0) { - pldmgr_log("[PLDMGR] Deleted payload: %s\n", filename); - resp = MHD_create_response_from_buffer(strlen(MSG_OK), (void *)MSG_OK, - MHD_RESPMEM_PERSISTENT); - } else { - const char *err = "Failed to delete file\n"; - resp = MHD_create_response_from_buffer(strlen(err), (void *)err, - MHD_RESPMEM_PERSISTENT); - } - } - } else { - const char *err = "Missing filename\n"; - resp = MHD_create_response_from_buffer(strlen(err), (void *)err, - MHD_RESPMEM_PERSISTENT); - } - MHD_add_response_header(resp, "Content-Type", "text/plain"); - } else if (strcmp(url, ROUTE_SHUTDOWN) == 0) { - const char *msg = "Payload Manager Core shutting down...\n"; - pldmgr_log("[PLDMGR] %s", msg); - resp = MHD_create_response_from_buffer(strlen(msg), (void *)msg, - MHD_RESPMEM_PERSISTENT); - MHD_add_response_header(resp, "Content-Type", "text/plain"); - keep_running = 0; /* Signal main loop to exit */ - } else if (strcmp(url, ROUTE_LOG) == 0) { - char *resp_buf; - resp = alloc_response_buffer(&resp_buf); - if (!resp) { - size_t pos = 0; - pos += snprintf(resp_buf + pos, RESPONSE_BUFFER_SIZE - pos, - "{\"logs\":["); - for (int i = 0; i < log_count; i++) { - int idx = (log_head - log_count + i + MAX_LOG_LINES) % MAX_LOG_LINES; - char escaped[MAX_LOG_LINE_LEN * 2]; - pldmgr_json_escape(log_buffer[idx], escaped, sizeof(escaped)); - pos += snprintf(resp_buf + pos, RESPONSE_BUFFER_SIZE - pos, - "\"%s\"%s", escaped, - (i == log_count - 1) ? "" : ","); - } - pos += snprintf(resp_buf + pos, RESPONSE_BUFFER_SIZE - pos, "]}"); - resp = MHD_create_response_from_buffer(pos, (void *)resp_buf, - MHD_RESPMEM_MUST_FREE); - MHD_add_response_header(resp, "Content-Type", "application/json"); - } - } else if (strcmp(url, ROUTE_VERSION) == 0) { - resp = MHD_create_response_from_buffer( - strlen(MENU_VERSION), (void *)MENU_VERSION, MHD_RESPMEM_PERSISTENT); - MHD_add_response_header(resp, "Content-Type", "text/plain"); - } else if (strcmp(url, ROUTE_GETIP) == 0) { - char ip[64]; - if (pldmgr_get_local_ip(ip, sizeof(ip)) != 0) { - strcpy(ip, "0.0.0.0"); - } - resp = MHD_create_response_from_buffer(strlen(ip), (void *)ip, - MHD_RESPMEM_MUST_COPY); - MHD_add_response_header(resp, "Content-Type", "text/plain"); - } else if (strcmp(url, ROUTE_AUTOLOAD_STATUS) == 0) { - int total, done; - char current[128]; - pldmgr_autoload_get_status(&total, &done, current); - int remaining = pldmgr_autoload_get_remaining_seconds(); - long long remaining_ms = pldmgr_autoload_get_remaining_ms(); - - char list_buf[4096] = ""; - FILE *f = fopen(AUTOLOAD_CONFIG_PATH, "r"); - if (f) { - char line[256]; - int first = 1; - while (fgets(line, sizeof(line), f)) { - line[strcspn(line, "\r\n")] = 0; - if (strlen(line) == 0 || line[0] == '!') - continue; - if (!first) - strcat(list_buf, ","); - strcat(list_buf, line); - first = 0; - } - fclose(f); - } - - int config_enabled, config_browser, config_delay, config_kill, config_usb, config_install, config_multi; - long config_repo; - read_next_config_values(&config_enabled, &config_repo, &config_browser, - &config_delay, &config_kill, &config_usb, &config_install, &config_multi); - - char current_escaped[256]; - char list_escaped[8192]; - pldmgr_json_escape(current, current_escaped, sizeof(current_escaped)); - pldmgr_json_escape(list_buf, list_escaped, sizeof(list_escaped)); - - char *resp_buf; - resp = alloc_response_buffer(&resp_buf); - if (!resp) { - snprintf(resp_buf, RESPONSE_BUFFER_SIZE, - "{\"remaining\":%d,\"remaining_ms\":%lld,\"total\":%d,\"done\":%d,\"current\":\"%s\"," - "\"list\":\"%s\",\"delay\":%d,\"KILL_DISC_PLAYER_ON_STARTUP\":%s,\"SCAN_USB_PAYLOADS\":%s}", - remaining, remaining_ms, total, done, current_escaped, list_escaped, config_delay, - config_kill ? "true" : "false", config_usb ? "true" : "false"); - resp = MHD_create_response_from_buffer(strlen(resp_buf), - (void *)resp_buf, - MHD_RESPMEM_MUST_FREE); - MHD_add_response_header(resp, "Content-Type", "application/json"); - } - } else if (strcmp(url, ROUTE_AUTOLOAD_CLEAR) == 0) { - pldmgr_autoload_reset(); - const char *msg = "Autoload status cleared.\n"; - resp = MHD_create_response_from_buffer(strlen(msg), (void *)msg, - MHD_RESPMEM_PERSISTENT); - MHD_add_response_header(resp, "Content-Type", "text/plain"); - } else if (strcmp(url, ROUTE_ABORT) == 0) { - pldmgr_autoload_abort(); - const char *msg = "Autoload sequence aborted.\n"; - resp = MHD_create_response_from_buffer(strlen(msg), (void *)msg, - MHD_RESPMEM_PERSISTENT); - MHD_add_response_header(resp, "Content-Type", "text/plain"); - } else if (strcmp(url, "/autoload_status") == 0) { - char resp_buf[64]; - snprintf(resp_buf, sizeof(resp_buf), "{\"remaining\":%d}", - pldmgr_autoload_get_remaining_seconds()); - resp = MHD_create_response_from_buffer(strlen(resp_buf), - (void *)resp_buf, - MHD_RESPMEM_MUST_COPY); - MHD_add_response_header(resp, "Content-Type", "application/json"); - } else if (strcmp(url, ROUTE_GET_CONFIG) == 0) { - int enabled = 0, browser_open = 1, auto_delay = 5, kill_disc = 1, scan_usb = 0, auto_install = 1, multi_src = 0; - long last_repo_update = 0; - read_next_config_values(&enabled, &last_repo_update, &browser_open, - &auto_delay, &kill_disc, &scan_usb, &auto_install, &multi_src); - - /* Get list */ - char list_buf[4096] = {0}; - FILE *f = fopen(AUTOLOAD_CONFIG_PATH, "r"); - if (f) { - char line[256]; - int first = 1; - while (fgets(line, sizeof(line), f)) { - line[strcspn(line, "\r\n")] = 0; - if (strlen(line) == 0) - continue; - if (!first) - strncat(list_buf, ",", sizeof(list_buf) - strlen(list_buf) - 1); - strncat(list_buf, line, sizeof(list_buf) - strlen(list_buf) - 1); - first = 0; - } - fclose(f); - } - - char list_escaped[8192]; - pldmgr_json_escape(list_buf, list_escaped, sizeof(list_escaped)); - - char *resp_buf; - resp = alloc_response_buffer(&resp_buf); - if (!resp) { - snprintf(resp_buf, RESPONSE_BUFFER_SIZE, - "{\"AUTOLOAD_ENABLED\":%s,\"AUTOLOAD_LIST\":\"%s\",\"LAST_" - "REPOSITORY_UPDATE\":%ld," - "\"AUTO_BROWSER_OPEN\":%s,\"AUTOLOAD_DELAY\":%d,\"KILL_DISC_" - "PLAYER_ON_STARTUP\":%s,\"SCAN_USB_PAYLOADS\":%s,\"AUTO_INSTALL_APP\":%s," - "\"MULTI_SOURCES_ENABLED\":%s}", - enabled ? "true" : "false", list_escaped, last_repo_update, - browser_open ? "true" : "false", auto_delay, - kill_disc ? "true" : "false", scan_usb ? "true" : "false", - auto_install ? "true" : "false", - multi_src ? "true" : "false"); - resp = MHD_create_response_from_buffer(strlen(resp_buf), - (void *)resp_buf, - MHD_RESPMEM_MUST_FREE); - MHD_add_response_header(resp, "Content-Type", "application/json"); - } - } else if (strcmp(url, "/events") == 0) { - struct LogSSEConnection *sse = malloc(sizeof(struct LogSSEConnection)); - sse->last_log_version = 0; - sse->sent_initial = 0; - - resp = MHD_create_response_from_callback( - MHD_SIZE_UNKNOWN, 1024, &log_stream_callback, sse, &log_stream_cleanup); - MHD_add_response_header(resp, "Content-Type", "text/event-stream"); - MHD_add_response_header(resp, "Cache-Control", "no-cache"); - MHD_add_response_header(resp, "Connection", "keep-alive"); - } else { - /* Default: 404 for now */ - const char *not_found = "404 Not Found\n"; - resp = MHD_create_response_from_buffer(strlen(not_found), (void *)not_found, - MHD_RESPMEM_PERSISTENT); - MHD_add_response_header(resp, "Content-Type", "text/plain"); - } - - if (!resp) - return MHD_NO; - - /* Add CORS headers */ - add_cors_headers(resp); - - ret = MHD_queue_response(conn, http_status, resp); - MHD_destroy_response(resp); - - return ret; + return pid; } /* PS5 System Calls (Internal) */ @@ -1375,163 +79,155 @@ extern int sceUserServiceInitialize(void *); __attribute__((used)) volatile const char pldmgr_version_sig[] = "PLDMGR_VER:" MENU_VERSION; int main(int argc, char *argv[]) { - struct MHD_Daemon *daemon; - unsigned short port = DEFAULT_PORT; - pid_t pid; + struct MHD_Daemon *daemon; + unsigned short port = DEFAULT_PORT; + pid_t pid; - syscall(SYS_thr_set_name, -1, "pldmgr.elf"); + syscall(SYS_thr_set_name, -1, "pldmgr.elf"); - while((pid=find_pid("pldmgr.elf")) > 0) { - if(kill(pid, SIGKILL)) { - pldmgr_log("[PLDMGR] kill failed\n"); - return EXIT_FAILURE; - } - sleep(1); - } - - pldmgr_log("[PLDMGR] Starting Payload Manager v%s on port %d...\n", pldmgr_version_sig + 11, - port); - - /* Check for Self-Update */ - char new_payload_path[512]; - if (payload_mgr_check_self_update(new_payload_path, sizeof(new_payload_path)) == 0) { - pldmgr_log("[PLDMGR] Found updated payload manager at %s. Launching...\n", new_payload_path); - ps5_launch_elf(new_payload_path); - return 0; /* Exit current process, new one will take over */ - } - - /* Initialize PS5 System Services */ - pldmgr_log("[PLDMGR] Initializing system services...\n"); - if (sceNetCtlInit() == 0) { - pldmgr_log("[PLDMGR] Network Controller initialized.\n"); - } - - int user_prio = 256; - if (sceUserServiceInitialize(&user_prio) == 0) { - pldmgr_log("[PLDMGR] User Service initialized.\n"); - } - - /* Start Autoload Sequence (if config exists) */ - int ex_en, ex_br = 1, ex_del = 5, ex_kill = 1, ex_usb = 0, ex_install = 1, ex_multi = 0; - long ex_repo = 0; - read_next_config_values(&ex_en, &ex_repo, &ex_br, &ex_del, &ex_kill, &ex_usb, &ex_install, &ex_multi); - - /* Install app if requested */ - if (ex_install) { - pldmgr_install_app_if_needed(); - } - - /* Kill Disc Player if running (BD-JB host) and enabled in config */ - if (ex_kill) { - ps5_kill_disc_player(); - } - - /* Signal Resilience */ - signal(SIGPIPE, SIG_IGN); - signal(SIGHUP, SIG_IGN); - signal(SIGTERM, SIG_IGN); - signal(SIGCONT, handle_sigcont); - - /* Start the MHD daemon */ - daemon = MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION | MHD_USE_DEBUG, port, - NULL, NULL, &on_request, NULL, MHD_OPTION_END); - - if (NULL == daemon) { - pldmgr_log("[PLDMGR] Failed to start HTTP daemon!\n"); - pldmgr_notify("Error: HTTP Server Failed\nPort 8084 may be busy"); - return 1; - } - - pldmgr_log("[PLDMGR] Server is running. Visit /shutdown to exit.\n"); - - /* Try cache refresh (no-op if network unavailable; browser push handles - * updates) */ - payload_mgr_repository_ensure_fresh(0); - - /* Automatically open browser to the menu URL (if enabled) */ - - /* Startup Notification - Only show if browser autostart is off */ - char current_ip[64] = "unknown"; - pldmgr_get_local_ip(current_ip, sizeof(current_ip)); - - if (!ex_br) { - if (strcmp(current_ip, "unknown") != 0) { - pldmgr_notify("Payload Manager v%s\nIP: %s\nPort: %d", MENU_VERSION, current_ip, - port); - } else { - pldmgr_notify("Payload Manager v%s\nWaiting for Network...", MENU_VERSION); - } - } - - /* Start Autoload Sequence (if config exists) */ - pldmgr_autoload_start(); - - if (ex_br) { - char browser_url[128]; - snprintf(browser_url, sizeof(browser_url), "http://127.0.0.1:%d", port); - ps5_launch_browser(browser_url); - } - - /* Watchdog and main loop */ - int network_check_timer = 0; - while (keep_running) { - usleep(100000); /* 100ms sleep */ - - /* Immediate Wake-up Recovery */ - if (resume_flag) { - resume_flag = 0; - pldmgr_log("[PLDMGR] Console resumed from standby. Refreshing network " - "stack...\n"); - pldmgr_autoload_reset(); /* Reset UI state if needed */ - - /* Force a check right now */ - network_check_timer = 50; - } - - /* Network Watchdog (every 5 seconds) */ - if (++network_check_timer >= 50) { - network_check_timer = 0; - char new_ip[64] = "unknown"; - int has_ip = (pldmgr_get_local_ip(new_ip, sizeof(new_ip)) == 0); - - /* If IP changed or we recovered from no-IP state, or if we just resumed - */ - if (has_ip && (strcmp(new_ip, current_ip) != 0 || - strcmp(current_ip, "unknown") == 0)) { - pldmgr_log("[PLDMGR] Network state refresh: %s -> %s. Restarting " - "server...\n", - current_ip, new_ip); - if (daemon) - MHD_stop_daemon(daemon); - - /* Give it a moment to release ports and for system to stabilize */ - usleep(800000); - - daemon = MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION | MHD_USE_DEBUG, - port, NULL, NULL, &on_request, NULL, - MHD_OPTION_END); - - if (daemon) { - strcpy(current_ip, new_ip); - pldmgr_log("[PLDMGR] Server restored on %s:%d\n", current_ip, port); - pldmgr_notify("Payload Manager: Service Restored\nIP: %s", current_ip); - } else { - pldmgr_log("[PLDMGR] !!! Failed to restore server!\n"); + while ((pid = find_pid("pldmgr.elf")) > 0) { + if (kill(pid, SIGKILL)) { + pldmgr_log("[PLDMGR] kill failed\n"); + return EXIT_FAILURE; } - } else if (!has_ip && strcmp(current_ip, "unknown") != 0) { - /* Lost connection */ - pldmgr_log("[PLDMGR] Network lost (was %s)\n", current_ip); - strcpy(current_ip, "unknown"); - } + sleep(1); } - } - pldmgr_log("[PLDMGR] Shutting down...\n"); - if (daemon) - MHD_stop_daemon(daemon); + pldmgr_log("[PLDMGR] Starting Payload Manager v%s on port %d...\n", + pldmgr_version_sig + 11, port); - /* Give some time for sockets to close before process exits */ - sleep(1); + /* Check for Self-Update */ + char new_payload_path[512]; + if (repository_check_self_update(new_payload_path, sizeof(new_payload_path)) == 0) { + pldmgr_log("[PLDMGR] Found updated payload manager at %s. Launching...\n", + new_payload_path); + ps5_launch_elf(new_payload_path); + return 0; + } - return 0; + /* Initialize PS5 System Services */ + pldmgr_log("[PLDMGR] Initializing system services...\n"); + if (sceNetCtlInit() == 0) { + pldmgr_log("[PLDMGR] Network Controller initialized.\n"); + } + + int user_prio = 256; + if (sceUserServiceInitialize(&user_prio) == 0) { + pldmgr_log("[PLDMGR] User Service initialized.\n"); + } + + /* Read startup config */ + PldmgrConfig cfg; + config_read(&cfg); + + /* Install app if requested */ + if (cfg.auto_install_app) { + pldmgr_install_app_if_needed(); + } + + /* Kill Disc Player if running (BD-JB host) and enabled in config */ + if (cfg.kill_disc_player) { + ps5_kill_disc_player(); + } + + /* Signal Resilience */ + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, SIG_IGN); + signal(SIGTERM, SIG_IGN); + signal(SIGCONT, handle_sigcont); + + /* Start the MHD daemon */ + daemon = MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION | MHD_USE_DEBUG, port, + NULL, NULL, &http_on_request, NULL, MHD_OPTION_END); + + if (NULL == daemon) { + pldmgr_log("[PLDMGR] Failed to start HTTP daemon!\n"); + pldmgr_notify("Error: HTTP Server Failed\nPort 8084 may be busy"); + return 1; + } + + pldmgr_log("[PLDMGR] Server is running. Visit /shutdown to exit.\n"); + + /* Try cache refresh */ + repository_ensure_fresh(0); + + /* Startup Notification - Only show if browser autostart is off */ + char current_ip[64] = "unknown"; + pldmgr_get_local_ip(current_ip, sizeof(current_ip)); + + if (!cfg.auto_browser_open) { + if (strcmp(current_ip, "unknown") != 0) { + pldmgr_notify("Payload Manager v%s\nIP: %s\nPort: %d", MENU_VERSION, + current_ip, port); + } else { + pldmgr_notify("Payload Manager v%s\nWaiting for Network...", MENU_VERSION); + } + } + + /* Start Autoload Sequence (if config exists) */ + pldmgr_autoload_start(); + + if (cfg.auto_browser_open) { + char browser_url[128]; + snprintf(browser_url, sizeof(browser_url), "http://127.0.0.1:%d", port); + ps5_launch_browser(browser_url); + } + + /* Watchdog and main loop */ + int network_check_timer = 0; + while (http_keep_running) { + usleep(100000); /* 100ms sleep */ + + /* Immediate Wake-up Recovery */ + if (resume_flag) { + resume_flag = 0; + pldmgr_log("[PLDMGR] Console resumed from standby. Refreshing network " + "stack...\n"); + pldmgr_autoload_reset(); + + /* Force a check right now */ + network_check_timer = 50; + } + + /* Network Watchdog (every 5 seconds) */ + if (++network_check_timer >= 50) { + network_check_timer = 0; + char new_ip[64] = "unknown"; + int has_ip = (pldmgr_get_local_ip(new_ip, sizeof(new_ip)) == 0); + + if (has_ip && (strcmp(new_ip, current_ip) != 0 || + strcmp(current_ip, "unknown") == 0)) { + pldmgr_log("[PLDMGR] Network state refresh: %s -> %s. Restarting " + "server...\n", + current_ip, new_ip); + if (daemon) + MHD_stop_daemon(daemon); + + usleep(800000); + + daemon = MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION | MHD_USE_DEBUG, + port, NULL, NULL, &http_on_request, NULL, + MHD_OPTION_END); + + if (daemon) { + strcpy(current_ip, new_ip); + pldmgr_log("[PLDMGR] Server restored on %s:%d\n", current_ip, port); + pldmgr_notify("Payload Manager: Service Restored\nIP: %s", current_ip); + } else { + pldmgr_log("[PLDMGR] !!! Failed to restore server!\n"); + } + } else if (!has_ip && strcmp(current_ip, "unknown") != 0) { + pldmgr_log("[PLDMGR] Network lost (was %s)\n", current_ip); + strcpy(current_ip, "unknown"); + } + } + } + + pldmgr_log("[PLDMGR] Shutting down...\n"); + if (daemon) + MHD_stop_daemon(daemon); + + sleep(1); + + return 0; } diff --git a/src/payload_mgr.c b/src/payload_mgr.c index 3b31694..676bef9 100644 --- a/src/payload_mgr.c +++ b/src/payload_mgr.c @@ -1,2354 +1,467 @@ +/* + * Payload Manager — Local payload storage management. + * + * Handles scanning, listing, importing, deleting, and USB transfer + * of ELF/BIN payload files on the PS5. + */ + +#include +#include #include #include #include -#include -#include -#include -#include -#include +#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include "assets_cacert_pem.h" - -int payload_mgr_repository_install_commit(const char *filename, const char *uploaded_temp_path, const char *install_source, const char *install_source_detail, char *msg_buf, size_t msg_buf_size); +#include +#include +#include "payload_mgr.h" +#include "json_helpers.h" +#include "sha256.h" +#include "repository.h" +#include "config.h" #include "pldmgr.h" -#include "autoload.h" +/* ── Extension / path checks ──────────────────────────────── */ - -static const char **scan_dirs = (const char **)SCAN_DIRS; -static const int scan_dirs_count = SCAN_DIRS_COUNT; - -typedef struct RepoPayload { - char name[128]; - char filename[256]; - char url[1024]; - char source[1024]; - char source_direct[1024]; - char description[1024]; - char last_update[64]; - char version[64]; - char checksum[65]; - char source_name[256]; /* human-readable name of the source that installed this payload */ -} RepoPayload; - -typedef struct JsonListBuilder { - char *buf; - size_t size; - size_t pos; - int first; -} JsonListBuilder; - -typedef struct SHA256_CTX { - unsigned char data[64]; - unsigned int datalen; - unsigned long long bitlen; - unsigned int state[8]; -} SHA256_CTX; - -static const unsigned int sha256_k[64] = { - 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, - 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, - 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, - 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, - 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, - 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, - 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, - 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, - 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, - 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, - 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, - 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, - 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, - 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, - 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, - 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 -}; - -#define SHA256_ROTR(a, b) (((a) >> (b)) | ((a) << (32 - (b)))) -#define SHA256_CH(x, y, z) (((x) & (y)) ^ (~(x) & (z))) -#define SHA256_MAJ(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) -#define SHA256_EP0(x) (SHA256_ROTR(x, 2) ^ SHA256_ROTR(x, 13) ^ SHA256_ROTR(x, 22)) -#define SHA256_EP1(x) (SHA256_ROTR(x, 6) ^ SHA256_ROTR(x, 11) ^ SHA256_ROTR(x, 25)) -#define SHA256_SIG0(x) (SHA256_ROTR(x, 7) ^ SHA256_ROTR(x, 18) ^ ((x) >> 3)) -#define SHA256_SIG1(x) (SHA256_ROTR(x, 17) ^ SHA256_ROTR(x, 19) ^ ((x) >> 10)) - -/* Helper to check if a file has a supported extension */ -static int is_supported_extension(const char *filename) { - const char *ext = strrchr(filename, '.'); - if (!ext) return 0; - - if (strcasecmp(ext, ".elf") == 0) return 1; - if (strcasecmp(ext, ".bin") == 0) return 1; - - return 0; +static int is_supported_extension(const char *path) { + const char *ext = strrchr(path, '.'); + if (!ext) + return 0; + return (strcasecmp(ext, ".elf") == 0 || strcasecmp(ext, ".bin") == 0); } static int is_allowed_usb_path(const char *path) { - if (!path) return 0; - size_t len = strlen(path); - if (len < 10) return 0; - if (strstr(path, "..")) return 0; - if (strncmp(path, "/mnt/usb", 8) != 0) return 0; - if (!isdigit((unsigned char)path[8])) return 0; - if (path[9] != '/') return 0; - return is_supported_extension(path); + if (!path || path[0] == '\0') + return 0; + if (strstr(path, "..")) + return 0; + if (!is_supported_extension(path)) + return 0; + if (strncmp(path, "/mnt/usb", 8) == 0 && path[8] >= '0' && path[8] <= '7' && + path[9] == '/') + return 1; + return 0; } - static void sha256_transform(SHA256_CTX *ctx, const unsigned char data[]) { - unsigned int a, b, c, d, e, f, g, h, i, j, t1, t2, m[64]; - - for (i = 0, j = 0; i < 16; i++, j += 4) { - m[i] = ((unsigned int)data[j] << 24) | ((unsigned int)data[j + 1] << 16) | - ((unsigned int)data[j + 2] << 8) | ((unsigned int)data[j + 3]); - } - for (; i < 64; i++) { - m[i] = SHA256_SIG1(m[i - 2]) + m[i - 7] + SHA256_SIG0(m[i - 15]) + m[i - 16]; - } - - a = ctx->state[0]; - b = ctx->state[1]; - c = ctx->state[2]; - d = ctx->state[3]; - e = ctx->state[4]; - f = ctx->state[5]; - g = ctx->state[6]; - h = ctx->state[7]; - - for (i = 0; i < 64; i++) { - t1 = h + SHA256_EP1(e) + SHA256_CH(e, f, g) + sha256_k[i] + m[i]; - t2 = SHA256_EP0(a) + SHA256_MAJ(a, b, c); - h = g; - g = f; - f = e; - e = d + t1; - d = c; - c = b; - b = a; - a = t1 + t2; - } - - ctx->state[0] += a; - ctx->state[1] += b; - ctx->state[2] += c; - ctx->state[3] += d; - ctx->state[4] += e; - ctx->state[5] += f; - ctx->state[6] += g; - ctx->state[7] += h; - } - - static void sha256_init(SHA256_CTX *ctx) { - ctx->datalen = 0; - ctx->bitlen = 0; - ctx->state[0] = 0x6a09e667; - ctx->state[1] = 0xbb67ae85; - ctx->state[2] = 0x3c6ef372; - ctx->state[3] = 0xa54ff53a; - ctx->state[4] = 0x510e527f; - ctx->state[5] = 0x9b05688c; - ctx->state[6] = 0x1f83d9ab; - ctx->state[7] = 0x5be0cd19; - } - - static void sha256_update(SHA256_CTX *ctx, const unsigned char data[], size_t len) { - size_t i; - for (i = 0; i < len; i++) { - ctx->data[ctx->datalen] = data[i]; - ctx->datalen++; - if (ctx->datalen == 64) { - sha256_transform(ctx, ctx->data); - ctx->bitlen += 512; - ctx->datalen = 0; - } - } - } - - static void sha256_final(SHA256_CTX *ctx, unsigned char hash[]) { - unsigned int i = ctx->datalen; - - if (ctx->datalen < 56) { - ctx->data[i++] = 0x80; - while (i < 56) { - ctx->data[i++] = 0x00; - } - } else { - ctx->data[i++] = 0x80; - while (i < 64) { - ctx->data[i++] = 0x00; - } - sha256_transform(ctx, ctx->data); - memset(ctx->data, 0, 56); - } - - ctx->bitlen += (unsigned long long)ctx->datalen * 8ULL; - ctx->data[63] = (unsigned char)(ctx->bitlen); - ctx->data[62] = (unsigned char)(ctx->bitlen >> 8); - ctx->data[61] = (unsigned char)(ctx->bitlen >> 16); - ctx->data[60] = (unsigned char)(ctx->bitlen >> 24); - ctx->data[59] = (unsigned char)(ctx->bitlen >> 32); - ctx->data[58] = (unsigned char)(ctx->bitlen >> 40); - ctx->data[57] = (unsigned char)(ctx->bitlen >> 48); - ctx->data[56] = (unsigned char)(ctx->bitlen >> 56); - sha256_transform(ctx, ctx->data); - - for (i = 0; i < 4; i++) { - hash[i] = (ctx->state[0] >> (24 - i * 8)) & 0x000000ff; - hash[i + 4] = (ctx->state[1] >> (24 - i * 8)) & 0x000000ff; - hash[i + 8] = (ctx->state[2] >> (24 - i * 8)) & 0x000000ff; - hash[i + 12] = (ctx->state[3] >> (24 - i * 8)) & 0x000000ff; - hash[i + 16] = (ctx->state[4] >> (24 - i * 8)) & 0x000000ff; - hash[i + 20] = (ctx->state[5] >> (24 - i * 8)) & 0x000000ff; - hash[i + 24] = (ctx->state[6] >> (24 - i * 8)) & 0x000000ff; - hash[i + 28] = (ctx->state[7] >> (24 - i * 8)) & 0x000000ff; - } - } - - static int compute_sha256_file(const char *path, char out_hex[65]) { - FILE *f = fopen(path, "rb"); - unsigned char hash[32]; - unsigned char buf[4096]; - size_t n; - SHA256_CTX ctx; - - if (!f) { - return -1; - } - - sha256_init(&ctx); - while ((n = fread(buf, 1, sizeof(buf), f)) > 0) { - sha256_update(&ctx, buf, n); - } - fclose(f); - - sha256_final(&ctx, hash); - for (int i = 0; i < 32; i++) { - snprintf(out_hex + (i * 2), 3, "%02x", hash[i]); - } - out_hex[64] = '\0'; - return 0; - } - - static int mkdir_if_missing(const char *path) { - struct stat st; - if (stat(path, &st) == 0) { - return S_ISDIR(st.st_mode) ? 0 : -1; - } - return mkdir(path, 0777); - } - - static int ensure_dir_recursive(const char *path) { - char tmp[512]; - size_t len = strlen(path); - if (len >= sizeof(tmp)) { - return -1; - } - - strncpy(tmp, path, sizeof(tmp)); - tmp[len] = '\0'; - - for (size_t i = 1; i < len; i++) { - if (tmp[i] == '/') { - tmp[i] = '\0'; - if (strlen(tmp) > 0 && mkdir_if_missing(tmp) != 0) { - return -1; - } - tmp[i] = '/'; - } - } - return mkdir_if_missing(tmp); - } - - static int json_append(JsonListBuilder *jb, const char *fmt, ...) { - va_list args; - int written; - - if (jb->pos >= jb->size) { - return -1; - } - - va_start(args, fmt); - written = vsnprintf(jb->buf + jb->pos, jb->size - jb->pos, fmt, args); - va_end(args); - - if (written < 0) { - return -1; - } - - if ((size_t)written >= jb->size - jb->pos) { - jb->pos = jb->size - 1; - jb->buf[jb->pos] = '\0'; - return -1; - } - - jb->pos += (size_t)written; - return 0; - } - - static int read_file_text(const char *path, char **out_buf, size_t *out_size) { - FILE *f; - long fsize; - char *buf; - size_t nread; - - *out_buf = NULL; - *out_size = 0; - - f = fopen(path, "rb"); - if (!f) { - return -1; - } - - if (fseek(f, 0, SEEK_END) != 0) { - fclose(f); - return -1; - } - fsize = ftell(f); - if (fsize < 0) { - fclose(f); - return -1; - } - if (fseek(f, 0, SEEK_SET) != 0) { - fclose(f); - return -1; - } - - buf = (char *)malloc((size_t)fsize + 1); - if (!buf) { - fclose(f); - return -1; - } - - nread = fread(buf, 1, (size_t)fsize, f); - fclose(f); - buf[nread] = '\0'; - - *out_buf = buf; - *out_size = nread; - return 0; - } - - static int write_file_text(const char *path, const char *data, size_t size) { - FILE *f = fopen(path, "wb"); - if (!f) { - return -1; - } - if (fwrite(data, 1, size, f) != size) { - fclose(f); - return -1; - } - fclose(f); - return 0; - } - - static size_t write_data(void *ptr, size_t size, size_t nmemb, FILE *stream) { - size_t written = fwrite(ptr, size, nmemb, stream); - return written; - } - - static int download_to_file(const char *url, const char *out_path) { - CURL *curl; - FILE *fp; - CURLcode res = CURLE_FAILED_INIT; - - if (!url || !out_path) { - pldmgr_log("[PLDMGR] download_to_file: missing url or path\n"); - return -1; - } - - fp = fopen(out_path, "wb"); - if (!fp) { - pldmgr_log("[PLDMGR] download_to_file: failed to open %s\n", out_path); - return -1; - } - - curl = curl_easy_init(); - if (curl) { - curl_easy_setopt(curl, CURLOPT_URL, url); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); - - // Set user agent - curl_easy_setopt(curl, CURLOPT_USERAGENT, "pldmgr/1.0"); - - // Securely verify SSL against embedded CA bundle - struct curl_blob blob; - blob.data = (void *)assets_cacert_pem; - blob.len = assets_cacert_pem_len; - blob.flags = CURL_BLOB_COPY; - - curl_easy_setopt(curl, CURLOPT_CAINFO_BLOB, &blob); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); - - // Allow redirection - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - - res = curl_easy_perform(curl); - - long http_code = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); - - curl_easy_cleanup(curl); - - if (res != CURLE_OK) { - pldmgr_log("[PLDMGR] curl_easy_perform failed: %s url=%s\n", curl_easy_strerror(res), url); - } else if (http_code != 200) { - pldmgr_log("[PLDMGR] HTTP status %ld for %s\n", http_code, url); - res = CURLE_HTTP_RETURNED_ERROR; - } - } - - fclose(fp); - - if (res != CURLE_OK) { - remove(out_path); - return -1; - } - - return 0; - } - - static int parse_config_last_update(long *out_ts) { - FILE *f; - char line[256]; - long ts = 0; - - *out_ts = 0; - f = fopen(PLDMGR_CONFIG_PATH, "r"); - if (!f) { - return 0; - } - - while (fgets(line, sizeof(line), f)) { - if (strncmp(line, "LAST_REPOSITORY_UPDATE=", 23) == 0) { - ts = atol(line + 23); - break; - } - } - fclose(f); - - *out_ts = ts; - return 0; - } - - static int upsert_next_config_value(const char *key, const char *value) { - FILE *f; - char line[256]; - char old_lines[64][256]; - int line_count = 0; - int replaced = 0; - size_t key_len = strlen(key); - - ensure_dir_recursive(BASE_DATA_DIR); - - f = fopen(PLDMGR_CONFIG_PATH, "r"); - if (f) { - while (line_count < 64 && fgets(line, sizeof(line), f)) { - if (strncmp(line, key, key_len) == 0 && line[key_len] == '=') { - snprintf(old_lines[line_count], sizeof(old_lines[line_count]), "%s=%s\n", key, value); - replaced = 1; - } else { - strncpy(old_lines[line_count], line, sizeof(old_lines[line_count])); - old_lines[line_count][sizeof(old_lines[line_count]) - 1] = '\0'; - } - line_count++; - } - fclose(f); - } - - if (!replaced && line_count < 64) { - snprintf(old_lines[line_count], sizeof(old_lines[line_count]), "%s=%s\n", key, value); - line_count++; - } - - f = fopen(PLDMGR_CONFIG_PATH, "w"); - if (!f) { - return -1; - } - for (int i = 0; i < line_count; i++) { - fputs(old_lines[i], f); - } - fclose(f); - return 0; - } - - static int write_config_last_update(long ts) { - char ts_buf[64]; - snprintf(ts_buf, sizeof(ts_buf), "%ld", ts); - return upsert_next_config_value("LAST_REPOSITORY_UPDATE", ts_buf); - } - - static int json_extract_string(const char *obj_start, const char *obj_end, const char *key, char *out, size_t out_size) { - char key_pattern[96]; - const char *p; - const char *colon; - const char *q; - size_t pos = 0; - - if (out_size == 0) { - return -1; - } - out[0] = '\0'; - - snprintf(key_pattern, sizeof(key_pattern), "\"%s\"", key); - p = strstr(obj_start, key_pattern); - if (!p || p >= obj_end) { - return -1; - } - - colon = strchr(p + strlen(key_pattern), ':'); - if (!colon || colon >= obj_end) { - return -1; - } - - q = colon + 1; - while (q < obj_end && isspace((unsigned char)*q)) { - q++; - } - if (q >= obj_end || *q != '"') { - return -1; - } - q++; - - while (q < obj_end && *q != '"') { - if (*q == '\\' && (q + 1) < obj_end) { - q++; - } - if (pos + 1 < out_size) { - out[pos++] = *q; - } - q++; - } - out[pos] = '\0'; - return 0; - } - - static int parse_repository_payloads(const char *json, RepoPayload **out_items, size_t *out_count) { - const char *p = json; - RepoPayload *items = NULL; - size_t count = 0; - - *out_items = NULL; - *out_count = 0; - - while ((p = strchr(p, '{')) != NULL) { - const char *end = strchr(p, '}'); - RepoPayload item; - RepoPayload *tmp; - if (!end) { - break; - } - - memset(&item, 0, sizeof(item)); - if (json_extract_string(p, end, "name", item.name, sizeof(item.name)) != 0 || - json_extract_string(p, end, "filename", item.filename, sizeof(item.filename)) != 0 || - json_extract_string(p, end, "url", item.url, sizeof(item.url)) != 0) { - p = end + 1; - continue; - } - - json_extract_string(p, end, "source", item.source, sizeof(item.source)); - json_extract_string(p, end, "source_direct", item.source_direct, sizeof(item.source_direct)); - json_extract_string(p, end, "description", item.description, sizeof(item.description)); - json_extract_string(p, end, "last_update", item.last_update, sizeof(item.last_update)); - json_extract_string(p, end, "version", item.version, sizeof(item.version)); - json_extract_string(p, end, "checksum", item.checksum, sizeof(item.checksum)); - - tmp = (RepoPayload *)realloc(items, (count + 1) * sizeof(RepoPayload)); - if (!tmp) { - free(items); - return -1; - } - items = tmp; - items[count] = item; - count++; - p = end + 1; - } - - *out_items = items; - *out_count = count; - return 0; - } - - static int load_cached_repository(RepoPayload **out_items, size_t *out_count) { - char *json = NULL; - size_t size = 0; - int ret; - - if (read_file_text(REPOSITORY_CACHE_PATH, &json, &size) != 0 || !json || size == 0) { - if (json) { - free(json); - } - return -1; - } - - ret = parse_repository_payloads(json, out_items, out_count); - free(json); - return ret; - } - - static int remove_regular_files_in_dir(const char *dir_path, const char *new_filename) { - DIR *dir = opendir(dir_path); - struct dirent *entry; - if (!dir) { - return 0; - } - - while ((entry = readdir(dir)) != NULL) { - char full_path[512]; - struct stat st; - - if (entry->d_name[0] == '.') { - continue; - } - - snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); - if (stat(full_path, &st) != 0) { - continue; - } - if (S_ISREG(st.st_mode)) { - if (is_supported_extension(entry->d_name)) { - pldmgr_autoload_update_config_entry(entry->d_name, new_filename); - } - remove(full_path); - } - } - - closedir(dir); - return 0; - } - - static int write_payload_details_json(const RepoPayload *item, const char *details_path, const char *install_source, const char *install_source_detail) { - char name[256], filename[384], url[1400], source[1400], source_direct[1400]; - char description[1400], last_update[128], version[128], checksum[128]; - char downloaded_at[64], i_src[256], i_detail[1400], i_src_name[256]; - char json_buf[8192]; - time_t now = time(NULL); - struct tm tmv; - - memset(&tmv, 0, sizeof(tmv)); - localtime_r(&now, &tmv); - strftime(downloaded_at, sizeof(downloaded_at), "%Y-%m-%dT%H:%M:%S%z", &tmv); - - pldmgr_json_escape(item->name, name, sizeof(name)); - pldmgr_json_escape(item->filename, filename, sizeof(filename)); - pldmgr_json_escape(item->url, url, sizeof(url)); - pldmgr_json_escape(item->source, source, sizeof(source)); - pldmgr_json_escape(item->source_direct, source_direct, sizeof(source_direct)); - pldmgr_json_escape(item->description, description, sizeof(description)); - pldmgr_json_escape(item->last_update, last_update, sizeof(last_update)); - pldmgr_json_escape(item->version, version, sizeof(version)); - pldmgr_json_escape(item->checksum, checksum, sizeof(checksum)); - pldmgr_json_escape(install_source ? install_source : "unknown", i_src, sizeof(i_src)); - pldmgr_json_escape(install_source_detail ? install_source_detail : "", i_detail, sizeof(i_detail)); - /* source_name: the human-readable name of the source that supplied this - * payload. Stored in item->source_direct when set by multi-source - * install; falls back to empty string for default/uploaded payloads. */ - pldmgr_json_escape(item->source_name[0] ? item->source_name : "", i_src_name, sizeof(i_src_name)); - - snprintf(json_buf, sizeof(json_buf), - "{\n" - " \"name\": \"%s\",\n" - " \"filename\": \"%s\",\n" - " \"url\": \"%s\",\n" - " \"source\": \"%s\",\n" - " \"source_direct\": \"%s\",\n" - " \"description\": \"%s\",\n" - " \"last_update\": \"%s\",\n" - " \"version\": \"%s\",\n" - " \"checksum\": \"%s\",\n" - " \"downloaded_at\": \"%s\",\n" - " \"install_source\": \"%s\",\n" - " \"install_source_detail\": \"%s\",\n" - " \"source_name\": \"%s\"\n" - "}\n", - name, filename, url, source, source_direct, description, - last_update, version, checksum, downloaded_at, i_src, i_detail, - i_src_name); - - FILE *f = fopen(details_path, "w"); - if (!f) return -1; - fwrite(json_buf, 1, strlen(json_buf), f); - fclose(f); - return 0; - } - - static int write_simple_payload_details_json(const char *filename, const char *details_path, const char *install_source, const char *install_source_detail) { - RepoPayload item; - memset(&item, 0, sizeof(item)); - strncpy(item.name, filename, sizeof(item.name) - 1); - strncpy(item.filename, filename, sizeof(item.filename) - 1); - return write_payload_details_json(&item, details_path, install_source, install_source_detail); - } - - int payload_mgr_write_metadata(const char *payload_path, const char *install_source, const char *install_source_detail) { - char details_path[700]; - const char *filename = strrchr(payload_path, '/'); - if (filename) filename++; - else filename = payload_path; - - snprintf(details_path, sizeof(details_path), "%s.json", payload_path); - return write_simple_payload_details_json(filename, details_path, install_source, install_source_detail); - } - - int payload_mgr_import_to_storage(const char *filename, const char *temp_path, const char *install_source, const char *install_source_detail, char *msg_buf, size_t msg_buf_size) { - char folder_name[128]; - char payload_dir[512]; - char final_path[640]; - char details_path[700]; - - pldmgr_utils_get_payload_folder_name(filename, folder_name, sizeof(folder_name)); - snprintf(payload_dir, sizeof(payload_dir), "%s/%s", PAYLOADS_STORAGE_DIR, folder_name); - - if (ensure_dir_recursive(payload_dir) != 0) { - snprintf(msg_buf, msg_buf_size, "Failed to create directory"); - return -1; - } - - snprintf(final_path, sizeof(final_path), "%s/%s", payload_dir, filename); - snprintf(details_path, sizeof(details_path), "%s/%s.json", payload_dir, filename); - - /* Clean up previous version if it has the same folder name but different filename */ - remove_regular_files_in_dir(payload_dir, filename); - - if (rename(temp_path, final_path) != 0) { - snprintf(msg_buf, msg_buf_size, "Failed to move file"); - return -1; - } - - write_simple_payload_details_json(filename, details_path, install_source, install_source_detail); - return 0; - } - - int payload_mgr_check_existing(const char *filename, char *out_json, size_t out_size) { - char folder_name[128]; - char folder_path[512]; - char file_path[640]; - struct stat st; - - pldmgr_utils_get_payload_folder_name(filename, folder_name, sizeof(folder_name)); - snprintf(folder_path, sizeof(folder_path), "%s/%s", PAYLOADS_STORAGE_DIR, folder_name); - snprintf(file_path, sizeof(file_path), "%s/%s", folder_path, filename); - - int folder_exists = (stat(folder_path, &st) == 0 && S_ISDIR(st.st_mode)); - int file_exists = (stat(file_path, &st) == 0 && S_ISREG(st.st_mode)); - - snprintf(out_json, out_size, "{\"status\":\"ok\", \"folder_exists\":%d, \"file_exists\":%d, \"folder_name\":\"%s\"}", - folder_exists, file_exists, folder_name); - return 0; - } - - static void scan_payloads_recursive(const char *dir_path, int depth, int max_depth, JsonListBuilder *jb) { - DIR *dir; - struct dirent *entry; - - if (depth > max_depth) { - return; - } - - dir = opendir(dir_path); - if (!dir) { - return; - } - - // pldmgr_log("[PLDMGR] Scanning: %s\n", dir_path); - - while ((entry = readdir(dir)) != NULL) { - char full_path[512]; - struct stat st; - - if (entry->d_name[0] == '.') { - continue; - } - - snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); - if (stat(full_path, &st) != 0) { - continue; - } - - if (S_ISDIR(st.st_mode)) { - scan_payloads_recursive(full_path, depth + 1, max_depth, jb); - continue; - } - - if (S_ISREG(st.st_mode) && is_supported_extension(entry->d_name)) { - pldmgr_log("[PLDMGR] Found payload: %s\n", full_path); - if (!jb->first) { - if (json_append(jb, ",") != 0) { - break; - } - } - if (json_append(jb, "\"%s\"", full_path) != 0) { - break; - } - jb->first = 0; - } - } - - closedir(dir); - } - - - static int resolve_recursive(const char *dir_path, const char *filename, char *out_path, size_t out_size, int depth) { - DIR *dir; - struct dirent *entry; - - if (depth > 6) { - return -1; - } - - dir = opendir(dir_path); - if (!dir) { - return -1; - } - - while ((entry = readdir(dir)) != NULL) { - char full_path[512]; - struct stat st; - - if (entry->d_name[0] == '.') { - continue; - } - - snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); - if (stat(full_path, &st) != 0) { - continue; - } - - if (S_ISDIR(st.st_mode)) { - if (resolve_recursive(full_path, filename, out_path, out_size, depth + 1) == 0) { - closedir(dir); - return 0; - } - continue; - } - - if (S_ISREG(st.st_mode) && strcmp(entry->d_name, filename) == 0) { - snprintf(out_path, out_size, "%s", full_path); - closedir(dir); - return 0; - } - } - - closedir(dir); +/* ── File utilities ────────────────────────────────────────── */ + +static int copy_file(const char *src, const char *dst) { + FILE *in = fopen(src, "rb"); + if (!in) + return -1; + FILE *out = fopen(dst, "wb"); + if (!out) { + fclose(in); return -1; } - - int pldmgr_read_config_bool(const char *key, int default_val) { - FILE *f = fopen(PLDMGR_CONFIG_PATH, "r"); - if (!f) return default_val; - char line[256]; - int res = default_val; - size_t key_len = strlen(key); - while (fgets(line, sizeof(line), f)) { - if (strncmp(line, key, key_len) == 0 && line[key_len] == '=') { - res = atoi(line + key_len + 1); - break; - } + char buf[8192]; + size_t n; + while ((n = fread(buf, 1, sizeof(buf), in)) > 0) { + if (fwrite(buf, 1, n, out) != n) { + fclose(in); + fclose(out); + return -1; } - fclose(f); - return res; } + fclose(in); + fclose(out); + return 0; +} - size_t payload_mgr_list_json(char *json_buf, size_t buf_size) { - JsonListBuilder jb; +/* Remove all files in a payload directory EXCEPT the new incoming file. + * Also updates the autoload config if a file is being replaced. */ +void payload_mgr_remove_old_files(const char *dir_path, const char *new_filename) { + DIR *d = opendir(dir_path); + if (!d) return; - ensure_dir_recursive(BASE_DATA_DIR); - ensure_dir_recursive(PAYLOADS_STORAGE_DIR); - - jb.buf = json_buf; - jb.size = buf_size; - jb.pos = 0; - jb.first = 1; - - if (json_append(&jb, "{\"payloads\":[") != 0) { - return 0; + struct dirent *entry; + while ((entry = readdir(d)) != NULL) { + if (entry->d_name[0] == '.') continue; + if (strcmp(entry->d_name, new_filename) == 0) continue; + /* Skip .json metadata files for the incoming file */ + { + size_t new_len = strlen(new_filename); + size_t ent_len = strlen(entry->d_name); + if (ent_len == new_len + 5 && + strncmp(entry->d_name, new_filename, new_len) == 0 && + strcmp(entry->d_name + new_len, ".json") == 0) + continue; } - for (int i = 0; i < scan_dirs_count; i++) { - scan_payloads_recursive(scan_dirs[i], 0, 5, &jb); + char full_path[640]; + snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); + + /* If it's a supported payload, update autoload config */ + if (is_supported_extension(entry->d_name)) { + pldmgr_autoload_update_config_entry(entry->d_name, new_filename); } - if (pldmgr_read_config_bool("SCAN_USB_PAYLOADS", 0)) { - for (int i = 0; i < 8; i++) { - char usb_root[32]; - snprintf(usb_root, sizeof(usb_root), "/mnt/usb%d", i); - scan_payloads_recursive(usb_root, 0, 0, &jb); - } - } - - if (json_append(&jb, "],") != 0 || - json_append(&jb, "\"meta\":{") != 0) { - return 0; - } - - /* Emit meta: walk PAYLOADS_STORAGE_DIR//*.elf.json sidecars. - * Each sidecar is named .json. Strip ".json" to get - * the key. Only emit entries where source_name is non-empty. */ - int meta_first = 1; - DIR *top = opendir(PAYLOADS_STORAGE_DIR); - if (top) { - struct dirent *sub; - while ((sub = readdir(top)) != NULL) { - if (sub->d_name[0] == '.') continue; - char subdir[512]; - snprintf(subdir, sizeof(subdir), "%s/%s", - PAYLOADS_STORAGE_DIR, sub->d_name); - DIR *d = opendir(subdir); - if (!d) continue; - struct dirent *de; - while ((de = readdir(d)) != NULL) { - const char *n = de->d_name; - size_t nl = strlen(n); - /* Must end with ".json" and the part before must end - * with .elf / .bin / .lua e.g. "kstuff.elf.json" */ - if (nl < 9) continue; - if (strcasecmp(n + nl - 5, ".json") != 0) continue; - /* base_len = length of name without ".json" */ - size_t base_len = nl - 5; - /* Check last 4 chars of base for payload extension */ - if (base_len < 4) continue; - char last4[5]; - memcpy(last4, n + base_len - 4, 4); - last4[4] = '\0'; - if (strcasecmp(last4, ".elf") != 0 && - strcasecmp(last4, ".bin") != 0 && - strcasecmp(last4, ".lua") != 0) continue; - - /* Read sidecar */ - char sidecar[700]; - snprintf(sidecar, sizeof(sidecar), "%s/%s", subdir, n); - char *sc_json = NULL; - size_t sc_size = 0; - if (read_file_text(sidecar, &sc_json, &sc_size) != 0 || !sc_json) - continue; - - /* Extract "source_name": "..." */ - char sn_val[256] = ""; - const char *sn_key = strstr(sc_json, "\"source_name\""); - if (sn_key) { - const char *colon = strchr(sn_key + 13, ':'); - if (colon) { - const char *q = colon + 1; - while (*q == ' ' || *q == '\t') q++; - if (*q == '"') { - q++; - size_t pos = 0; - while (*q && *q != '"' && pos < sizeof(sn_val) - 1) - sn_val[pos++] = *q++; - sn_val[pos] = '\0'; - } - } - } - - free(sc_json); - if (!sn_val[0]) continue; /* local/uploaded payload — no badge */ - - /* key = filename without trailing ".json" */ - char key[300]; - if (base_len >= sizeof(key)) base_len = sizeof(key) - 1; - memcpy(key, n, base_len); - key[base_len] = '\0'; - - char fn_esc[350], sn_esc[300]; - pldmgr_json_escape(key, fn_esc, sizeof(fn_esc)); - pldmgr_json_escape(sn_val, sn_esc, sizeof(sn_esc)); - char entry[750]; - snprintf(entry, sizeof(entry), - "%s\"%s\":{\"source_name\":\"%s\"}", - meta_first ? "" : ",", fn_esc, sn_esc); - if (json_append(&jb, entry) != 0) { - closedir(d); - closedir(top); - goto meta_done; - } - meta_first = 0; - } - closedir(d); - } - closedir(top); - } - meta_done: - json_append(&jb, "}}"); - return jb.pos; + remove(full_path); } + closedir(d); +} - static int copy_file(const char *src, const char *dst) { - FILE *fs = fopen(src, "rb"); - if (!fs) return -1; - FILE *fd = fopen(dst, "wb"); - if (!fd) { - fclose(fs); - return -1; - } - unsigned char buf[8192]; - size_t n; - while ((n = fread(buf, 1, sizeof(buf), fs)) > 0) { - if (fwrite(buf, 1, n, fd) != n) { - fclose(fs); - fclose(fd); - return -1; - } - } - fclose(fs); - fclose(fd); - return 0; - } - - int payload_mgr_usb_check(const char *usb_path, char *out_json, size_t out_size) { - char usb_sha[65]; - char int_sha[65]; - char internal_path[512]; - const char *filename = strrchr(usb_path, '/'); - if (!filename) filename = usb_path; - else filename++; - - if (!is_allowed_usb_path(usb_path)) { - snprintf(out_json, out_size, "{\"error\":\"Invalid path\"}"); - return -1; - } - - if (compute_sha256_file(usb_path, usb_sha) != 0) { - snprintf(out_json, out_size, "{\"error\":\"Failed to compute USB SHA256\"}"); - return -1; - } - - char folder_name[128]; - pldmgr_utils_get_payload_folder_name(filename, folder_name, sizeof(folder_name)); - snprintf(internal_path, sizeof(internal_path), "%s/%s/%s", PAYLOADS_STORAGE_DIR, folder_name, filename); - - char folder_path[512]; - snprintf(folder_path, sizeof(folder_path), "%s/%s", PAYLOADS_STORAGE_DIR, folder_name); - - struct stat st; - int folder_exists = (stat(folder_path, &st) == 0 && S_ISDIR(st.st_mode)); - - if (stat(internal_path, &st) == 0) { - if (compute_sha256_file(internal_path, int_sha) == 0) { - if (strcmp(usb_sha, int_sha) == 0) { - snprintf(out_json, out_size, "{\"status\":\"exists_same\", \"filename\":\"%s\", \"sha256\":\"%s\", \"folder_exists\":%d}", filename, usb_sha, folder_exists); - } else { - snprintf(out_json, out_size, "{\"status\":\"exists_different\", \"filename\":\"%s\", \"sha256\":\"%s\", \"folder_exists\":%d}", filename, usb_sha, folder_exists); - } - } else { - snprintf(out_json, out_size, "{\"status\":\"exists_error\", \"filename\":\"%s\", \"folder_exists\":%d}", filename, folder_exists); - } - } else { - snprintf(out_json, out_size, "{\"status\":\"new\", \"filename\":\"%s\", \"sha256\":\"%s\", \"folder_exists\":%d}", filename, usb_sha, folder_exists); - } - - return 0; - } - - int payload_mgr_usb_move(const char *usb_path, int overwrite, char *out_json, size_t out_size) { - char usb_sha[65]; - char check_sha[65]; - char temp_path[512]; - const char *filename = strrchr(usb_path, '/'); - if (!filename) filename = usb_path; - else filename++; - - if (!is_allowed_usb_path(usb_path)) { - snprintf(out_json, out_size, "{\"error\":\"Invalid path\"}"); - return -1; - } - - if (!overwrite) { - char folder_name[128]; - char final_path[640]; - pldmgr_utils_get_payload_folder_name(filename, folder_name, sizeof(folder_name)); - snprintf(final_path, sizeof(final_path), "%s/%s/%s", PAYLOADS_STORAGE_DIR, folder_name, filename); - if (access(final_path, F_OK) == 0) { - snprintf(out_json, out_size, "{\"error\":\"File exists\"}"); - return -1; - } - } - - snprintf(temp_path, sizeof(temp_path), "%s/%s.tmp", PAYLOADS_STORAGE_DIR, filename); - - if (compute_sha256_file(usb_path, usb_sha) != 0) { - snprintf(out_json, out_size, "{\"error\":\"Failed to compute USB SHA256\"}"); - return -1; - } - - ensure_dir_recursive(PAYLOADS_STORAGE_DIR); - if (copy_file(usb_path, temp_path) != 0) { - snprintf(out_json, out_size, "{\"error\":\"Failed to copy file to internal storage\"}"); - remove(temp_path); - return -1; - } - - if (compute_sha256_file(temp_path, check_sha) != 0 || strcmp(usb_sha, check_sha) != 0) { - snprintf(out_json, out_size, "{\"error\":\"SHA256 verification failed after copy\"}"); - remove(temp_path); - return -1; - } - - char msg[256]; - if (payload_mgr_import_to_storage(filename, temp_path, "usb_move", usb_path, msg, sizeof(msg)) != 0) { - snprintf(out_json, out_size, "{\"error\":\"%s\"}", msg); - remove(temp_path); - return -1; - } - - if (remove(usb_path) != 0) { - pldmgr_log("[PLDMGR] Warning: Failed to remove original file from USB: %s\n", usb_path); - snprintf(out_json, out_size, "{\"status\":\"ok\", \"warning\":\"copied but failed to delete from usb\"}"); - } else { - snprintf(out_json, out_size, "{\"status\":\"ok\"}"); - } - - return 0; - } - - int payload_mgr_resolve_path(const char *filename, char *out_path, size_t out_size) { - for (int i = 0; i < SCAN_DIRS_COUNT; i++) { - if (resolve_recursive(SCAN_DIRS[i], filename, out_path, out_size, 0) == 0) { - return 0; - } - } - return -1; - } - - int payload_mgr_delete_payload_file(const char *filename) { - char path[512]; - char details_path[640]; - char dir_path[512]; - char *last_slash; - - if (!filename || strstr(filename, "/") || strstr(filename, "..")) { - return -1; - } - - if (payload_mgr_resolve_path(filename, path, sizeof(path)) != 0) { - return -1; - } - - if (remove(path) != 0) { - return -1; - } - - snprintf(details_path, sizeof(details_path), "%s.json", path); - remove(details_path); - - /* Remove from autoload.txt if present */ - pldmgr_autoload_update_config_entry(filename, NULL); - - /* Try to remove the containing directory (will fail if not empty) */ - strncpy(dir_path, path, sizeof(dir_path)); - dir_path[sizeof(dir_path) - 1] = '\0'; - last_slash = strrchr(dir_path, '/'); - if (last_slash) { - *last_slash = '\0'; - rmdir(dir_path); - } - - return 0; - } - - /* - * payload_mgr_repository_push_json - * - * Called when the browser POSTs the raw ps5_payloads.json content it - * fetched over HTTPS. Validates the JSON, then atomically replaces the - * local cache so subsequent /repository_payloads calls work offline. - */ - int payload_mgr_repository_push_json(const char *json, size_t len) { - RepoPayload *items = NULL; - size_t count = 0; - char tmp_path[512]; - time_t now = time(NULL); - - if (!json || len == 0) { - pldmgr_log("[PLDMGR] !!! repository_push: empty body\n"); - return -1; - } - - if (parse_repository_payloads(json, &items, &count) != 0 || count == 0) { - pldmgr_log("[PLDMGR] !!! repository_push: JSON parse failed or 0 entries\n"); - if (items) free(items); - return -1; - } - free(items); - - ensure_dir_recursive(BASE_DATA_DIR); - ensure_dir_recursive(PAYLOADS_STORAGE_DIR); - - snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", REPOSITORY_CACHE_PATH); - if (write_file_text(tmp_path, json, len) != 0) { - remove(tmp_path); - return -1; - } - if (rename(tmp_path, REPOSITORY_CACHE_PATH) != 0) { - remove(tmp_path); - return -1; - } - - write_config_last_update((long)now); - pldmgr_log("[PLDMGR] Repository cache updated via browser push (%zu entries)\n", count); - return 0; - } - - int payload_mgr_repository_ensure_fresh(int force_refresh) { - long last_update = 0; - time_t now = time(NULL); - char tmp_path[512]; - char *json = NULL; - size_t json_size = 0; - RepoPayload *items = NULL; - size_t count = 0; - - ensure_dir_recursive(BASE_DATA_DIR); - ensure_dir_recursive(PAYLOADS_STORAGE_DIR); - - parse_config_last_update(&last_update); - - if (!force_refresh && access(REPOSITORY_CACHE_PATH, F_OK) == 0) { - long delta = (long)now - last_update; - if (delta >= 0 && delta < REPOSITORY_REFRESH_INTERVAL_SEC) { - return 0; - } - } - - snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", REPOSITORY_CACHE_PATH); - if (download_to_file(REPOSITORY_SOURCE_URL, tmp_path) != 0) { - return -1; - } - - if (read_file_text(tmp_path, &json, &json_size) != 0 || json_size == 0) { - remove(tmp_path); - if (json) { - free(json); - } - return -1; - } - - if (parse_repository_payloads(json, &items, &count) != 0 || count == 0) { - pldmgr_log("[PLDMGR] !!! Repository payload JSON parse failed\n"); - free(items); - free(json); - remove(tmp_path); - return -1; - } - - free(items); - free(json); - - if (rename(tmp_path, REPOSITORY_CACHE_PATH) != 0) { - remove(tmp_path); - return -1; - } - - write_config_last_update((long)now); - pldmgr_log("[PLDMGR] Repository cache refreshed (%ld entries timestamp)\n", (long)count); - return 0; - } - - size_t payload_mgr_repository_list_json(char *json_buf, size_t buf_size, int force_refresh) { - char *cached = NULL; - size_t cached_size = 0; - long last_update = 0; - size_t pos = 0; - - if (force_refresh) { - if (payload_mgr_repository_ensure_fresh(1) != 0) { - parse_config_last_update(&last_update); - snprintf(json_buf, buf_size, - "{\"payloads\":[],\"last_update\":%ld,\"cache_status\":\"error\"}", - last_update); - return strlen(json_buf); - } - } - - parse_config_last_update(&last_update); - - if (read_file_text(REPOSITORY_CACHE_PATH, &cached, &cached_size) != 0 || cached_size == 0) { - if (cached) { - free(cached); - } - snprintf(json_buf, buf_size, - "{\"payloads\":[],\"last_update\":%ld,\"cache_status\":\"missing\"}", - last_update); - return strlen(json_buf); - } - - pos += (size_t)snprintf(json_buf + pos, buf_size - pos, - "{\"payloads\":"); - if (pos >= buf_size || buf_size - pos <= 64 || cached_size > (buf_size - pos - 64)) { - snprintf(json_buf, buf_size, - "{\"payloads\":[],\"last_update\":%ld,\"cache_status\":\"truncated\"}", - last_update); - free(cached); - return strlen(json_buf); - } - if (pos < buf_size) { - size_t copy_len = cached_size; - memcpy(json_buf + pos, cached, copy_len); - pos += copy_len; - } - - pos += (size_t)snprintf(json_buf + pos, buf_size - pos, - ",\"last_update\":%ld,\"cache_status\":\"ok\",\"repo_url\":\"%s\"}", - last_update, REPOSITORY_SOURCE_URL); - - free(cached); - return pos; - } - - int payload_mgr_repository_install_download(const char *filename, const char *install_source_detail, char *msg_buf, size_t msg_buf_size) { - RepoPayload *items = NULL; - size_t count = 0; - int found = -1; - char tmp_path[512]; - - if (msg_buf && msg_buf_size > 0) { - msg_buf[0] = '\0'; - } - - if (!filename || strlen(filename) == 0 || strstr(filename, "/") || strstr(filename, "..")) { - snprintf(msg_buf, msg_buf_size, "Invalid filename"); - return -1; - } - - if (load_cached_repository(&items, &count) != 0 || count == 0) { - if (payload_mgr_repository_ensure_fresh(1) == 0) { - if (load_cached_repository(&items, &count) != 0 || count == 0) { - snprintf(msg_buf, msg_buf_size, "Repository cache missing or invalid"); - if (items) { - free(items); - } - return -1; - } - } else { - snprintf(msg_buf, msg_buf_size, "Repository cache missing or invalid"); - if (items) { - free(items); - } - return -1; - } - } - - for (size_t i = 0; i < count; i++) { - if (strcmp(items[i].filename, filename) == 0) { - found = (int)i; - break; - } - } - - if (found < 0) { - free(items); - snprintf(msg_buf, msg_buf_size, "Payload not found in repository"); - return -1; - } - - ensure_dir_recursive(PAYLOADS_STORAGE_DIR); - snprintf(tmp_path, sizeof(tmp_path), "%s/%s.part", PAYLOADS_STORAGE_DIR, items[found].filename); - - if (download_to_file(items[found].url, tmp_path) != 0) { - free(items); - snprintf(msg_buf, msg_buf_size, "Download failed"); - remove(tmp_path); - return -1; - } - - const char *detail = (install_source_detail && install_source_detail[0]) - ? install_source_detail - : REPOSITORY_SOURCE_URL; - - if (payload_mgr_repository_install_commit(items[found].filename, tmp_path, - "repository", detail, - msg_buf, msg_buf_size) != 0) { - free(items); - return -1; - } - - free(items); - return 0; - } - - int payload_mgr_repository_install_commit(const char *filename, const char *uploaded_temp_path, const char *install_source, const char *install_source_detail, char *msg_buf, size_t msg_buf_size) { - RepoPayload *items = NULL; - size_t count = 0; - int found = -1; - char payload_dir[512]; - char final_path[640]; - char details_path[700]; - - if (!filename || strlen(filename) == 0 || strstr(filename, "/") || strstr(filename, "..")) { - snprintf(msg_buf, msg_buf_size, "Invalid filename"); - return -1; - } - - if (load_cached_repository(&items, &count) != 0 || count == 0) { - if (payload_mgr_repository_ensure_fresh(1) == 0) { - if (load_cached_repository(&items, &count) != 0 || count == 0) { - snprintf(msg_buf, msg_buf_size, "Repository cache missing or invalid"); - if (items) { - free(items); - } - return -1; - } - } else { - snprintf(msg_buf, msg_buf_size, "Repository cache missing or invalid"); - if (items) { - free(items); - } - return -1; - } - } - - if (count == 0) { - snprintf(msg_buf, msg_buf_size, "Repository cache missing or invalid"); - if (items) { - free(items); - } - return -1; - } - - for (size_t i = 0; i < count; i++) { - if (strcmp(items[i].filename, filename) == 0) { - found = (int)i; - break; - } - } - - if (found < 0) { - free(items); - snprintf(msg_buf, msg_buf_size, "Payload not found in repository"); - return -1; - } - - char folder_name[128]; - pldmgr_utils_get_payload_folder_name(items[found].filename, folder_name, sizeof(folder_name)); - snprintf(payload_dir, sizeof(payload_dir), "%s/%s", PAYLOADS_STORAGE_DIR, folder_name); - if (ensure_dir_recursive(payload_dir) != 0) { - free(items); - snprintf(msg_buf, msg_buf_size, "Failed to create payload directory"); - return -1; - } - - snprintf(final_path, sizeof(final_path), "%s/%s", payload_dir, items[found].filename); - snprintf(details_path, sizeof(details_path), "%s/%s.json", payload_dir, items[found].filename); - - if (strlen(items[found].checksum) == 64) { - char calculated[65]; - if (compute_sha256_file(uploaded_temp_path, calculated) != 0) { - free(items); - remove(uploaded_temp_path); - snprintf(msg_buf, msg_buf_size, "Checksum computation failed"); - return -1; - } - if (strcasecmp(calculated, items[found].checksum) != 0) { - pldmgr_log("[PLDMGR] !!! Checksum mismatch for %s\n", items[found].filename); - free(items); - remove(uploaded_temp_path); - snprintf(msg_buf, msg_buf_size, "Checksum mismatch"); - return -1; - } - } - - /* Verify succeeded, now clear previous payload and metadata. */ - remove_regular_files_in_dir(payload_dir, items[found].filename); - - if (rename(uploaded_temp_path, final_path) != 0) { - free(items); - remove(uploaded_temp_path); - snprintf(msg_buf, msg_buf_size, "Failed to finalize payload file"); - return -1; - } - - if (write_payload_details_json(&items[found], details_path, install_source, install_source_detail) != 0) { - free(items); - remove(final_path); - snprintf(msg_buf, msg_buf_size, "Failed to write payload metadata"); - return -1; - } - - pldmgr_log("[PLDMGR] Repository payload installed: %s -> %s\n", items[found].filename, final_path); - snprintf(msg_buf, msg_buf_size, "Installed %s", items[found].filename); - free(items); - return 0; - } - - int get_elf_pldmgr_version(const char *path, char *out_version, size_t out_size) { - FILE *f = fopen(path, "rb"); - if (!f) return -1; - - char sig[16]; - sig[0] = 'P'; sig[1] = 'L'; sig[2] = 'D'; sig[3] = 'M'; sig[4] = 'G'; - sig[5] = 'R'; sig[6] = '_'; sig[7] = 'V'; sig[8] = 'E'; sig[9] = 'R'; sig[10] = ':'; sig[11] = '\0'; - size_t sig_len = 11; - - char buffer[8192]; - size_t bytes_read; - int match_idx = 0; - - while ((bytes_read = fread(buffer, 1, sizeof(buffer), f)) > 0) { - for (size_t i = 0; i < bytes_read; i++) { - if (buffer[i] == sig[match_idx]) { - match_idx++; - if (match_idx == sig_len) { - size_t v_idx = 0; - i++; - while (i < bytes_read && buffer[i] != '\0' && v_idx < out_size - 1) { - out_version[v_idx++] = buffer[i++]; - } - if (i < bytes_read && buffer[i] == '\0') { - if (v_idx > 0) { - out_version[v_idx] = '\0'; - fclose(f); - return 0; - } else { - match_idx = 0; - continue; - } - } else { - while (v_idx < out_size - 1) { - int c = fgetc(f); - if (c == EOF || c == '\0') break; - out_version[v_idx++] = (char)c; - } - out_version[v_idx] = '\0'; - if (v_idx > 0) { - fclose(f); - return 0; - } else { - match_idx = 0; - continue; - } - } - } - } else { - match_idx = 0; - if (buffer[i] == sig[0]) match_idx = 1; - } - } - } - fclose(f); - return -1; - } - - static int parse_version_part(const char **p) { - int val = 0; - while (**p >= '0' && **p <= '9') { - val = val * 10 + (**p - '0'); - (*p)++; - } - if (**p == '.') (*p)++; - return val; - } - - static int compare_versions(const char *v1, const char *v2) { - const char *p1 = v1; - const char *p2 = v2; - - int maj1 = parse_version_part(&p1); - int maj2 = parse_version_part(&p2); - if (maj1 != maj2) return maj1 - maj2; - - int min1 = parse_version_part(&p1); - int min2 = parse_version_part(&p2); - if (min1 != min2) return min1 - min2; - - int pat1 = parse_version_part(&p1); - int pat2 = parse_version_part(&p2); - - int res = pat1 - pat2; - pldmgr_log("[PLDMGR] Comparing versions: %s vs %s -> %d\n", v1, v2, res); - return res; - } - - int payload_mgr_check_self_update(char *out_path, size_t out_size) { - DIR *dir = opendir(PAYLOADS_STORAGE_DIR); - if (!dir) return -1; - - struct dirent *entry; - while ((entry = readdir(dir)) != NULL) { - if (entry->d_name[0] == '.') continue; - - /* Check for pldmgr or payload-manager directory (case-insensitive) */ - if (strcasecmp(entry->d_name, "pldmgr") == 0 || strcasecmp(entry->d_name, "payload-manager") == 0) { - char subdir_path[512]; - snprintf(subdir_path, sizeof(subdir_path), "%s/%s", PAYLOADS_STORAGE_DIR, entry->d_name); - - struct stat st; - if (stat(subdir_path, &st) == 0 && S_ISDIR(st.st_mode)) { - DIR *sdir = opendir(subdir_path); - if (sdir) { - struct dirent *sentry; - while ((sentry = readdir(sdir)) != NULL) { - if (sentry->d_name[0] == '.') continue; - - /* Look for .elf or .bin files that match our name */ - if (strstr(sentry->d_name, ".elf") || strstr(sentry->d_name, ".bin")) { - if (strcasestr(sentry->d_name, "pldmgr") || strcasestr(sentry->d_name, "payload-manager")) { - char full_path[512]; - snprintf(full_path, sizeof(full_path), "%s/%s", subdir_path, sentry->d_name); - - /* Verify ELF Magic */ - unsigned char magic[4]; - FILE *ef = fopen(full_path, "rb"); - if (ef) { - if (fread(magic, 1, 4, ef) == 4 && magic[0] == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F') { - fclose(ef); - char version[64]; - if (get_elf_pldmgr_version(full_path, version, sizeof(version)) == 0) { - pldmgr_log("[PLDMGR] Found potential update: %s (v%s)\n", full_path, version); - if (compare_versions(version, MENU_VERSION) > 0) { - strncpy(out_path, full_path, out_size); - closedir(sdir); - closedir(dir); - return 0; - } - } - } else { - fclose(ef); - } - } - } - } - } - closedir(sdir); - } - } - } - } - - closedir(dir); - return -1; - } - -/* ========================================================= - * SOURCE MANAGEMENT - * ========================================================= */ +/* ── Recursive scan ────────────────────────────────────────── */ typedef struct { - char id[64]; - char name[256]; - char url[1024]; - int removable; -} SourceEntry; + char path[512]; + char filename[256]; + int is_storage; + long mtime; +} PayloadEntry; -/* - * Build the sources array from SOURCES_CONFIG_PATH. - * If the file is missing or corrupt, returns a default-only list. - */ -static int load_sources(SourceEntry *out, int *out_count) { - char *json = NULL; - size_t size = 0; - int count = 0; +static void scan_payloads_recursive(const char *dir_path, int is_storage, + PayloadEntry **out, int *count, int max) { + DIR *d = opendir(dir_path); + if (!d) return; - memset(out, 0, sizeof(SourceEntry) * MAX_SOURCES); + struct dirent *entry; + while (*count < max && (entry = readdir(d)) != NULL) { + if (entry->d_name[0] == '.') + continue; - /* Always start with the default source at index 0 */ - strncpy(out[0].id, "default", sizeof(out[0].id) - 1); - strncpy(out[0].name, "Official Repository", sizeof(out[0].name) - 1); - strncpy(out[0].url, REPOSITORY_SOURCE_URL, sizeof(out[0].url) - 1); - out[0].removable = 0; - count = 1; + char full_path[512]; + snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); - if (read_file_text(SOURCES_CONFIG_PATH, &json, &size) != 0 || !json || size == 0) { - if (json) free(json); - *out_count = count; - return 0; - } + struct stat st; + if (stat(full_path, &st) != 0) + continue; - /* Find "sources": [...] array */ - const char *arr_start = strstr(json, "\"sources\""); - if (!arr_start) { free(json); *out_count = count; return 0; } - - arr_start = strchr(arr_start, '['); - if (!arr_start) { free(json); *out_count = count; return 0; } - - const char *p = arr_start + 1; - while (count < MAX_SOURCES && (p = strchr(p, '{')) != NULL) { - const char *end = strchr(p, '}'); - if (!end) break; - - char id[64] = "", name[256] = "", url[1024] = ""; - char removable_str[8] = ""; - - json_extract_string(p, end, "id", id, sizeof(id)); - json_extract_string(p, end, "name", name, sizeof(name)); - json_extract_string(p, end, "url", url, sizeof(url)); - json_extract_string(p, end, "removable", removable_str, sizeof(removable_str)); - - /* Skip the default entry stored in file — we already added it */ - if (strcmp(id, "default") == 0) { p = end + 1; continue; } - - if (name[0] && url[0] && id[0]) { - strncpy(out[count].id, id, sizeof(out[count].id) - 1); - strncpy(out[count].name, name, sizeof(out[count].name) - 1); - strncpy(out[count].url, url, sizeof(out[count].url) - 1); - out[count].removable = (strcmp(removable_str, "false") != 0); - count++; + if (S_ISDIR(st.st_mode)) { + scan_payloads_recursive(full_path, is_storage, out, count, max); + continue; } - p = end + 1; - } - free(json); - *out_count = count; - return 0; + if (!is_supported_extension(entry->d_name)) + continue; + + PayloadEntry *tmp = realloc(*out, (*count + 1) * sizeof(PayloadEntry)); + if (!tmp) break; + *out = tmp; + + strncpy((*out)[*count].path, full_path, sizeof((*out)[*count].path) - 1); + (*out)[*count].path[sizeof((*out)[*count].path) - 1] = '\0'; + strncpy((*out)[*count].filename, entry->d_name, sizeof((*out)[*count].filename) - 1); + (*out)[*count].filename[sizeof((*out)[*count].filename) - 1] = '\0'; + (*out)[*count].is_storage = is_storage; + (*out)[*count].mtime = (long)st.st_mtime; + (*count)++; + } + closedir(d); } -static int save_sources(SourceEntry *sources, int count) { - char buf[16384]; - JsonListBuilder jb = { buf, sizeof(buf), 0, 1 }; +/* ── Recursive resolve ─────────────────────────────────────── */ - ensure_dir_recursive(BASE_DATA_DIR); +static int resolve_recursive(const char *dir_path, const char *filename, + char *out_path, size_t out_size) { + DIR *d = opendir(dir_path); + if (!d) return -1; - json_append(&jb, "{\"sources\":[\n"); - for (int i = 0; i < count; i++) { - char id_e[128], name_e[512], url_e[2048]; - pldmgr_json_escape(sources[i].id, id_e, sizeof(id_e)); - pldmgr_json_escape(sources[i].name, name_e, sizeof(name_e)); - pldmgr_json_escape(sources[i].url, url_e, sizeof(url_e)); - if (json_append(&jb, " {\"id\":\"%s\",\"name\":\"%s\",\"url\":\"%s\",\"removable\":%s}%s\n", - id_e, name_e, url_e, - sources[i].removable ? "true" : "false", - (i < count - 1) ? "," : "") != 0) { - break; - } - } - json_append(&jb, "]}\n"); + struct dirent *entry; + while ((entry = readdir(d)) != NULL) { + if (entry->d_name[0] == '.') + continue; - return write_file_text(SOURCES_CONFIG_PATH, buf, jb.pos); -} + char full[512]; + snprintf(full, sizeof(full), "%s/%s", dir_path, entry->d_name); -int payload_mgr_sources_list_json(char *buf, size_t size) { - SourceEntry sources[MAX_SOURCES] = {0}; - int count = 0; - load_sources(sources, &count); + struct stat st; + if (stat(full, &st) != 0) + continue; - JsonListBuilder jb = { buf, size, 0, 1 }; - buf[0] = '\0'; - - json_append(&jb, "{\"sources\":[\n"); - for (int i = 0; i < count; i++) { - char id_e[128], name_e[512], url_e[2048]; - pldmgr_json_escape(sources[i].id, id_e, sizeof(id_e)); - pldmgr_json_escape(sources[i].name, name_e, sizeof(name_e)); - pldmgr_json_escape(sources[i].url, url_e, sizeof(url_e)); - if (json_append(&jb, " {\"id\":\"%s\",\"name\":\"%s\",\"url\":\"%s\",\"removable\":%s}%s\n", - id_e, name_e, url_e, - sources[i].removable ? "true" : "false", - (i < count - 1) ? "," : "") != 0) { - break; - } - } - json_append(&jb, "]}\n"); - return 0; -} - -int payload_mgr_sources_save(const char *json, size_t len) { - SourceEntry sources[MAX_SOURCES] = {0}; - int count = 0; - - if (!json || len == 0) return -1; - - /* Always put default at slot 0 */ - strncpy(sources[0].id, "default", sizeof(sources[0].id) - 1); - strncpy(sources[0].name, "Official Repository", sizeof(sources[0].name) - 1); - strncpy(sources[0].url, REPOSITORY_SOURCE_URL, sizeof(sources[0].url) - 1); - sources[0].removable = 0; - count = 1; - - const char *arr_start = strstr(json, "\"sources\""); - if (!arr_start) return -1; - arr_start = strchr(arr_start, '['); - if (!arr_start) return -1; - - const char *p = arr_start + 1; - while (count < MAX_SOURCES && (p = strchr(p, '{')) != NULL) { - const char *end = strchr(p, '}'); - if (!end) break; - - char id[64] = "", name[256] = "", url[1024] = ""; - json_extract_string(p, end, "id", id, sizeof(id)); - json_extract_string(p, end, "name", name, sizeof(name)); - json_extract_string(p, end, "url", url, sizeof(url)); - - if (strcmp(id, "default") == 0) { p = end + 1; continue; } - if (!name[0] || !url[0] || !id[0]) { p = end + 1; continue; } - - strncpy(sources[count].id, id, sizeof(sources[count].id) - 1); - strncpy(sources[count].name, name, sizeof(sources[count].name) - 1); - strncpy(sources[count].url, url, sizeof(sources[count].url) - 1); - sources[count].removable = 1; - count++; - p = end + 1; - } - - return save_sources(sources, count); -} - -/* - * Download the JSON at url into a temp buffer, validate it has a "name" field - * and at least one payload, then append it to the sources list. - */ -int payload_mgr_sources_add(const char *url, char *msg_buf, size_t msg_size) { - char tmp_path[512]; - char *json = NULL; - size_t json_size = 0; - RepoPayload *items = NULL; - size_t count = 0; - char source_name[256] = ""; - - if (!url || !url[0]) { - snprintf(msg_buf, msg_size, "URL is empty"); - return -1; - } - - ensure_dir_recursive(BASE_DATA_DIR); - snprintf(tmp_path, sizeof(tmp_path), "%s/source_validate.tmp", BASE_DATA_DIR); - - if (download_to_file(url, tmp_path) != 0) { - snprintf(msg_buf, msg_size, "URL unreachable or download failed"); - remove(tmp_path); - return -1; - } - - if (read_file_text(tmp_path, &json, &json_size) != 0 || !json || json_size == 0) { - snprintf(msg_buf, msg_size, "Failed to read downloaded JSON"); - remove(tmp_path); - if (json) free(json); - return -1; - } - remove(tmp_path); - - /* Read top-level "name" field. - * Only search BEFORE the "payloads" key to avoid picking up individual - * payload items' own "name" fields. - * If there is no "payloads" key the JSON is a raw array — no top-level - * name is possible, so skip the search entirely. */ - { - const char *payloads_key = strstr(json, "\"payloads\""); - if (payloads_key) { - /* Only scan the header section before "payloads" */ - size_t search_len = (size_t)(payloads_key - json); - const char *name_key = NULL; - for (size_t i = 0; i + 6 <= search_len; i++) { - if (memcmp(json + i, "\"name\"", 6) == 0) { - name_key = json + i; - break; - } - } - if (name_key) { - const char *colon = strchr(name_key + 6, ':'); - if (colon && (size_t)(colon - json) < search_len) { - const char *q = colon + 1; - while (*q == ' ' || *q == '\t') q++; - if (*q == '"') { - q++; - size_t pos = 0; - while (*q && *q != '"' && pos < sizeof(source_name) - 1) - source_name[pos++] = *q++; - source_name[pos] = '\0'; - } - } + if (S_ISDIR(st.st_mode)) { + if (resolve_recursive(full, filename, out_path, out_size) == 0) { + closedir(d); + return 0; } + continue; } - /* If still empty, fall back to the hostname portion of the URL */ - if (!source_name[0]) { - /* Skip scheme: "http://" or "https://" */ - const char *host = strstr(url, "://"); - host = host ? host + 3 : url; - size_t pos = 0; - while (*host && *host != '/' && *host != ':' && - pos < sizeof(source_name) - 1) - source_name[pos++] = *host++; - source_name[pos] = '\0'; - } - } - /* The JSON may be in two formats: - * 1. { "name": "...", "payloads": [...] } <-- new format with name - * 2. { "payloads": [...] } <-- legacy format (array at root or under "payloads") - * parse_repository_payloads handles both since it just looks for '{' objects */ - if (parse_repository_payloads(json, &items, &count) != 0 || count == 0) { - free(json); - if (items) free(items); - snprintf(msg_buf, msg_size, "No valid payloads found in source JSON"); - return -1; - } - free(items); - free(json); - - /* Load current sources and append */ - SourceEntry sources[MAX_SOURCES] = {0}; - int src_count = 0; - load_sources(sources, &src_count); - - if (src_count >= MAX_SOURCES) { - snprintf(msg_buf, msg_size, "Maximum number of sources (%d) reached", MAX_SOURCES); - return -1; - } - - /* Check for duplicate URL */ - for (int i = 0; i < src_count; i++) { - if (strcmp(sources[i].url, url) == 0) { - snprintf(msg_buf, msg_size, "Source already added"); - return -1; - } - } - - /* Assign a unique ID */ - char new_id[64]; - snprintf(new_id, sizeof(new_id), "source_%d", (int)time(NULL)); - - strncpy(sources[src_count].id, new_id, sizeof(sources[src_count].id) - 1); - strncpy(sources[src_count].name, source_name, sizeof(sources[src_count].name) - 1); - strncpy(sources[src_count].url, url, sizeof(sources[src_count].url) - 1); - sources[src_count].removable = 1; - src_count++; - - if (save_sources(sources, src_count) != 0) { - snprintf(msg_buf, msg_size, "Failed to save sources"); - return -1; - } - - /* Return the source name in msg_buf for the caller to forward to the UI */ - snprintf(msg_buf, msg_size, "%s", source_name); - return 0; -} - -int payload_mgr_sources_remove(int index, char *msg_buf, size_t msg_size) { - if (index <= 0) { - snprintf(msg_buf, msg_size, "Cannot remove the default source"); - return -1; - } - - SourceEntry sources[MAX_SOURCES] = {0}; - int count = 0; - load_sources(sources, &count); - - if (index >= count) { - snprintf(msg_buf, msg_size, "Invalid source index"); - return -1; - } - - /* Shift entries down */ - for (int i = index; i < count - 1; i++) { - sources[i] = sources[i + 1]; - } - count--; - - if (save_sources(sources, count) != 0) { - snprintf(msg_buf, msg_size, "Failed to save sources"); - return -1; - } - - snprintf(msg_buf, msg_size, "OK"); - return 0; -} - -/* ========================================================= - * MULTI-SOURCE REPOSITORY - * ========================================================= */ - -/* - * Get the cache file path for a given source index. - * Index 0 (default) uses the original REPOSITORY_CACHE_PATH. - */ -static void get_source_cache_path(int index, char *out, size_t out_size) { - if (index == 0) { - strncpy(out, REPOSITORY_CACHE_PATH, out_size - 1); - out[out_size - 1] = '\0'; - } else { - snprintf(out, out_size, "%s.src%d.json", REPOSITORY_CACHE_PATH, index); - } -} - -/* - * Ensure the cache for a specific source URL is fresh. - * Returns 0 on success, -1 on failure. - */ -static int ensure_source_fresh(const char *url, const char *cache_path, int force_refresh) { - long last_update = 0; - time_t now = time(NULL); - char tmp_path[512]; - char *json = NULL; - size_t json_size = 0; - RepoPayload *items = NULL; - size_t count = 0; - - ensure_dir_recursive(BASE_DATA_DIR); - - /* Read last update time from the cache file's companion .ts file */ - char ts_path[512]; - snprintf(ts_path, sizeof(ts_path), "%s.ts", cache_path); - FILE *tf = fopen(ts_path, "r"); - if (tf) { - fscanf(tf, "%ld", &last_update); - fclose(tf); - } - - if (!force_refresh && access(cache_path, F_OK) == 0) { - long delta = (long)now - last_update; - if (delta >= 0 && delta < REPOSITORY_REFRESH_INTERVAL_SEC) { + if (strcmp(entry->d_name, filename) == 0) { + strncpy(out_path, full, out_size - 1); + out_path[out_size - 1] = '\0'; + closedir(d); return 0; } } - - snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", cache_path); - if (download_to_file(url, tmp_path) != 0) { - return -1; - } - - if (read_file_text(tmp_path, &json, &json_size) != 0 || json_size == 0) { - if (json) free(json); - remove(tmp_path); - return -1; - } - - /* Validate: must have at least 1 payload */ - if (parse_repository_payloads(json, &items, &count) != 0 || count == 0) { - free(items); - free(json); - remove(tmp_path); - return -1; - } - free(items); - free(json); - - if (rename(tmp_path, cache_path) != 0) { - remove(tmp_path); - return -1; - } - - /* Write timestamp */ - tf = fopen(ts_path, "w"); - if (tf) { - fprintf(tf, "%ld", (long)now); - fclose(tf); - } - - return 0; + closedir(d); + return -1; } -/* - * Build JSON grouped by source: - * { - * "sources": [ - * { "id": "default", "name": "Official Repository", "payload_count": 5, - * "last_update": 123456, "error": false, - * "payloads": [ {...}, ... ] }, - * ... - * ] - * } - */ -size_t payload_mgr_multi_repository_list_json(char *buf, size_t size, int force_refresh) { - SourceEntry sources[MAX_SOURCES] = {0}; - int src_count = 0; - load_sources(sources, &src_count); +/* ── Public API ────────────────────────────────────────────── */ - JsonListBuilder jb = { buf, size, 0, 1 }; - buf[0] = '\0'; +size_t payload_mgr_list_json(char *json_buf, size_t buf_size) { + PayloadEntry *payloads = NULL; + int count = 0; + int max = 256; + int scan_usb = config_read_bool("SCAN_USB_PAYLOADS", 0); - json_append(&jb, "{\"sources\":[\n"); + /* Scan internal storage */ + scan_payloads_recursive(PAYLOADS_STORAGE_DIR, 1, &payloads, &count, max); - for (int si = 0; si < src_count; si++) { - char cache_path[512]; - get_source_cache_path(si, cache_path, sizeof(cache_path)); + /* Scan pldmgr subdirs */ + for (int i = 0; i < SCAN_DIRS_COUNT; i++) { + scan_payloads_recursive(SCAN_DIRS[i], 0, &payloads, &count, max); + } - int fetch_ok = (ensure_source_fresh(sources[si].url, cache_path, force_refresh) == 0); - - char id_e[128], name_e[512]; - pldmgr_json_escape(sources[si].id, id_e, sizeof(id_e)); - pldmgr_json_escape(sources[si].name, name_e, sizeof(name_e)); - - /* Read last_update timestamp */ - long last_update = 0; - char ts_path[512]; - snprintf(ts_path, sizeof(ts_path), "%s.ts", cache_path); - FILE *tf = fopen(ts_path, "r"); - if (tf) { fscanf(tf, "%ld", &last_update); fclose(tf); } - - if (json_append(&jb, " {\"id\":\"%s\",\"name\":\"%s\",\"last_update\":%ld,\"error\":%s,\"payloads\":[\n", - id_e, name_e, last_update, fetch_ok ? "false" : "true") != 0) { - break; - } - - if (fetch_ok) { - char *cached = NULL; - size_t cached_size = 0; - RepoPayload *items = NULL; - size_t count = 0; - - if (read_file_text(cache_path, &cached, &cached_size) == 0 && cached) { - parse_repository_payloads(cached, &items, &count); - free(cached); - } - - for (size_t pi = 0; pi < count; pi++) { - char name_pe[256], filename_e[512], desc_e[2048]; - char ver_e[128], url_e[2048], src_id_e[128], src_name_e[512]; - - pldmgr_json_escape(items[pi].name, name_pe, sizeof(name_pe)); - pldmgr_json_escape(items[pi].filename, filename_e, sizeof(filename_e)); - pldmgr_json_escape(items[pi].description, desc_e, sizeof(desc_e)); - pldmgr_json_escape(items[pi].version, ver_e, sizeof(ver_e)); - pldmgr_json_escape(items[pi].url, url_e, sizeof(url_e)); - pldmgr_json_escape(sources[si].id, src_id_e, sizeof(src_id_e)); - pldmgr_json_escape(sources[si].name, src_name_e, sizeof(src_name_e)); - - if (json_append(&jb, " {\"name\":\"%s\",\"filename\":\"%s\",\"description\":\"%s\"," - "\"version\":\"%s\",\"url\":\"%s\"," - "\"source_id\":\"%s\",\"source_name\":\"%s\"}%s\n", - name_pe, filename_e, desc_e, ver_e, url_e, - src_id_e, src_name_e, - (pi < count - 1) ? "," : "") != 0) { - break; - } - } - - if (items) free(items); - } - - if (json_append(&jb, " ]}%s\n", (si < src_count - 1) ? "," : "") != 0) { - break; + /* Optionally scan USB root directories */ + if (scan_usb) { + for (int usb = 0; usb < 8; usb++) { + char usb_root[64]; + snprintf(usb_root, sizeof(usb_root), "/mnt/usb%d", usb); + struct stat st; + if (stat(usb_root, &st) == 0 && S_ISDIR(st.st_mode)) + scan_payloads_recursive(usb_root, 0, &payloads, &count, max); } } - json_append(&jb, "]}\n"); + /* Build JSON response */ + JsonListBuilder jb = { json_buf, buf_size, 0, 1 }; + json_append(&jb, "{\"payloads\":["); + + for (int i = 0; i < count; i++) { + char path_escaped[1024], filename_escaped[512]; + char details_buf[4096] = ""; + int has_details = 0; + + pldmgr_json_escape(payloads[i].filename, filename_escaped, sizeof(filename_escaped)); + pldmgr_json_escape(payloads[i].path, path_escaped, sizeof(path_escaped)); + + /* Try to load sidecar .json metadata */ + char details_path[640]; + snprintf(details_path, sizeof(details_path), "%s.json", payloads[i].path); + char *details_json = NULL; + size_t details_size = 0; + if (read_file_text(details_path, &details_json, &details_size) == 0 && details_json) { + /* Extract fields from sidecar */ + char d_name[256] = "", d_desc[1024] = "", d_ver[64] = "", d_url[1024] = ""; + char d_src[1024] = "", d_src_direct[1024] = "", d_last_update[64] = ""; + char d_checksum[65] = "", d_downloaded[64] = ""; + char d_install_src[128] = "", d_install_detail[1024] = ""; + char d_source_name[256] = ""; + + const char *end = details_json + details_size; + json_extract_string(details_json, end, "name", d_name, sizeof(d_name)); + json_extract_string(details_json, end, "description", d_desc, sizeof(d_desc)); + json_extract_string(details_json, end, "version", d_ver, sizeof(d_ver)); + json_extract_string(details_json, end, "url", d_url, sizeof(d_url)); + json_extract_string(details_json, end, "source", d_src, sizeof(d_src)); + json_extract_string(details_json, end, "source_direct", d_src_direct, sizeof(d_src_direct)); + json_extract_string(details_json, end, "last_update", d_last_update, sizeof(d_last_update)); + json_extract_string(details_json, end, "checksum", d_checksum, sizeof(d_checksum)); + json_extract_string(details_json, end, "downloaded_at", d_downloaded, sizeof(d_downloaded)); + json_extract_string(details_json, end, "install_source", d_install_src, sizeof(d_install_src)); + json_extract_string(details_json, end, "install_source_detail", d_install_detail, sizeof(d_install_detail)); + json_extract_string(details_json, end, "source_name", d_source_name, sizeof(d_source_name)); + + char ne[512], de[2048], ve[128], ue[2048], se[2048], sde[2048]; + char lue[128], ce[128], dae[128], ise[256], ide[2048], sne[512]; + pldmgr_json_escape(d_name, ne, sizeof(ne)); + pldmgr_json_escape(d_desc, de, sizeof(de)); + pldmgr_json_escape(d_ver, ve, sizeof(ve)); + pldmgr_json_escape(d_url, ue, sizeof(ue)); + pldmgr_json_escape(d_src, se, sizeof(se)); + pldmgr_json_escape(d_src_direct, sde, sizeof(sde)); + pldmgr_json_escape(d_last_update, lue, sizeof(lue)); + pldmgr_json_escape(d_checksum, ce, sizeof(ce)); + pldmgr_json_escape(d_downloaded, dae, sizeof(dae)); + pldmgr_json_escape(d_install_src, ise, sizeof(ise)); + pldmgr_json_escape(d_install_detail, ide, sizeof(ide)); + pldmgr_json_escape(d_source_name, sne, sizeof(sne)); + + snprintf(details_buf, sizeof(details_buf), + ",\"display_name\":\"%s\",\"description\":\"%s\",\"version\":\"%s\"," + "\"url\":\"%s\",\"source\":\"%s\",\"source_direct\":\"%s\"," + "\"last_update\":\"%s\",\"checksum\":\"%s\",\"downloaded_at\":\"%s\"," + "\"install_source\":\"%s\",\"install_source_detail\":\"%s\"," + "\"source_name\":\"%s\"", + ne, de, ve, ue, se, sde, lue, ce, dae, ise, ide, sne); + has_details = 1; + free(details_json); + } + + json_append(&jb, "%s{\"name\":\"%s\",\"filename\":\"%s\",\"path\":\"%s\"," + "\"is_storage\":%s,\"mtime\":%ld%s}", + (i > 0) ? "," : "", + filename_escaped, filename_escaped, path_escaped, + payloads[i].is_storage ? "true" : "false", + payloads[i].mtime, + has_details ? details_buf : ""); + } + + json_append(&jb, "]}"); + + if (payloads) + free(payloads); + return jb.pos; } -int payload_mgr_multi_repository_install(const char *filename, const char *source_id, const char *repo_url, char *msg, size_t msg_size) { - SourceEntry sources[MAX_SOURCES] = {0}; - int src_count = 0; - load_sources(sources, &src_count); - - /* Find the matching source */ - int si = -1; - for (int i = 0; i < src_count; i++) { - if (strcmp(sources[i].id, source_id) == 0) { si = i; break; } - } - - if (si < 0) { - /* Fall back to matching by URL */ - for (int i = 0; i < src_count; i++) { - if (repo_url && strcmp(sources[i].url, repo_url) == 0) { si = i; break; } - } - } - - if (si < 0) { - /* Last resort: use default */ - si = 0; - } - - char cache_path[512]; - get_source_cache_path(si, cache_path, sizeof(cache_path)); - - /* Ensure cache is fresh */ - if (access(cache_path, F_OK) != 0) { - if (ensure_source_fresh(sources[si].url, cache_path, 1) != 0) { - snprintf(msg, msg_size, "Failed to fetch source repository"); - return -1; - } - } - - /* Load cached payloads for this source */ - char *cached = NULL; - size_t cached_size = 0; - RepoPayload *items = NULL; - size_t count = 0; - - if (read_file_text(cache_path, &cached, &cached_size) != 0 || !cached) { - snprintf(msg, msg_size, "Repository cache unavailable"); - return -1; - } - - parse_repository_payloads(cached, &items, &count); - free(cached); - - /* Find the requested payload */ - int found = -1; - for (size_t i = 0; i < count; i++) { - if (strcmp(items[i].filename, filename) == 0) { found = (int)i; break; } - } - - if (found < 0) { - if (items) free(items); - snprintf(msg, msg_size, "Payload not found in source"); - return -1; - } - - /* Download */ - ensure_dir_recursive(PAYLOADS_STORAGE_DIR); - char tmp_path[512]; - snprintf(tmp_path, sizeof(tmp_path), "%s/%s.part", PAYLOADS_STORAGE_DIR, items[found].filename); - - if (download_to_file(items[found].url, tmp_path) != 0) { - if (items) free(items); - remove(tmp_path); - snprintf(msg, msg_size, "Download failed"); - return -1; - } - - /* Commit — reuse existing logic with the source URL as the detail */ - const char *detail = (repo_url && repo_url[0]) ? repo_url : sources[si].url; - - /* Use install_commit directly with the loaded item data */ - char folder_name[128]; - char payload_dir[512]; - char final_path[640]; - char details_path[700]; - - pldmgr_utils_get_payload_folder_name(items[found].filename, folder_name, sizeof(folder_name)); - snprintf(payload_dir, sizeof(payload_dir), "%s/%s", PAYLOADS_STORAGE_DIR, folder_name); - - if (ensure_dir_recursive(payload_dir) != 0) { - if (items) free(items); - remove(tmp_path); - snprintf(msg, msg_size, "Failed to create payload directory"); - return -1; - } - - snprintf(final_path, sizeof(final_path), "%s/%s", payload_dir, items[found].filename); - snprintf(details_path, sizeof(details_path), "%s/%s.json", payload_dir, items[found].filename); - - /* Checksum verification (optional — skip if checksum absent) */ - if (strlen(items[found].checksum) == 64) { - char calculated[65]; - if (compute_sha256_file(tmp_path, calculated) != 0) { - if (items) free(items); - remove(tmp_path); - snprintf(msg, msg_size, "Checksum computation failed"); - return -1; - } - if (strcasecmp(calculated, items[found].checksum) != 0) { - pldmgr_log("[PLDMGR] !!! Checksum mismatch for %s\n", items[found].filename); - if (items) free(items); - remove(tmp_path); - snprintf(msg, msg_size, "Checksum mismatch"); - return -1; - } - } - - remove_regular_files_in_dir(payload_dir, items[found].filename); - - if (rename(tmp_path, final_path) != 0) { - if (items) free(items); - remove(tmp_path); - snprintf(msg, msg_size, "Failed to finalize payload file"); - return -1; - } - - /* Attach source_id and source_name to the item so metadata captures them */ - strncpy(items[found].source, sources[si].id, sizeof(items[found].source) - 1); - strncpy(items[found].source_direct, sources[si].name, sizeof(items[found].source_direct) - 1); - strncpy(items[found].source_name, sources[si].name, sizeof(items[found].source_name) - 1); - - if (write_payload_details_json(&items[found], details_path, "repository", detail) != 0) { - if (items) free(items); - snprintf(msg, msg_size, "Failed to write payload metadata"); - return -1; - } - - pldmgr_log("[PLDMGR] Multi-source install complete: %s (source: %s)\n", - items[found].filename, sources[si].name); - snprintf(msg, msg_size, "Installed %s", items[found].filename); - if (items) free(items); - return 0; -} - -/* ========================================================= - * PROCESSES MANAGEMENT - * ========================================================= */ - -#define PAGE_SIZE 16384 -#define MiB(x) ((x) / (1024.0 * 1024)) - -typedef struct app_info { - uint32_t app_id; - uint64_t unknown1; - char title_id[14]; - char unknown2[0x3c]; -} app_info_t; - -extern int sceKernelGetAppInfo(pid_t pid, app_info_t *info); - -static int is_user_daemon(const char *name, uint32_t app_id) { - if (!name) return 0; - - // Specifically exclude mini-syscore.elf - if (strcmp(name, "mini-syscore.elf") == 0) return 0; - - // Must have app_id == 0000 - if (app_id != 0) return 0; - - const char *ext = strrchr(name, '.'); - if (ext && strcasecmp(ext, ".elf") == 0) return 1; - - return 0; -} - -size_t payload_mgr_process_list_json(char *buf, size_t max_size) { - int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0}; - size_t buf_size = 0; - void *sysctl_buf = NULL; - - JsonListBuilder jb = { buf, max_size, 0, 1 }; - buf[0] = '\0'; - - json_append(&jb, "{\"processes\":[\n"); - - if (sysctl(mib, 4, NULL, &buf_size, NULL, 0) == 0) { - sysctl_buf = malloc(buf_size); - if (sysctl_buf) { - if (sysctl(mib, 4, sysctl_buf, &buf_size, NULL, 0) == 0) { - int count = 0; - for (void *ptr = sysctl_buf; ptr < (sysctl_buf + buf_size);) { - struct kinfo_proc *ki = (struct kinfo_proc*)ptr; - if (ki->ki_structsize == 0) break; - ptr += ki->ki_structsize; - - app_info_t appinfo; - if(sceKernelGetAppInfo(ki->ki_pid, &appinfo)) { - memset(&appinfo, 0, sizeof(appinfo)); - } - - int is_daemon = is_user_daemon(ki->ki_comm, appinfo.app_id); - - char name_e[512]; - pldmgr_json_escape(ki->ki_comm, name_e, sizeof(name_e)); - - double mem_mib = MiB(ki->ki_rssize * PAGE_SIZE); - - if (json_append(&jb, "%s {\"pid\":%d,\"name\":\"%s\",\"memory\":%.1f,\"is_daemon\":%s}", - (count > 0) ? ",\n" : "", - (int)ki->ki_pid, name_e, mem_mib, is_daemon ? "true" : "false") != 0) { - break; - } - count++; - } - } - free(sysctl_buf); - } - } - - json_append(&jb, "\n]}\n"); - return jb.pos; -} - -int payload_mgr_process_kill(int pid) { - if (pid <= 0) return -1; // Prevent killing kernel or init - - // Prevent killing our own process - if (pid == getpid()) return -1; - - if (kill(pid, SIGKILL) == 0) { - return 0; +int payload_mgr_resolve_path(const char *filename, char *out_path, size_t out_size) { + for (int i = 0; i < SCAN_DIRS_COUNT; i++) { + if (resolve_recursive(SCAN_DIRS[i], filename, out_path, out_size) == 0) + return 0; } return -1; } + +int payload_mgr_delete_payload_file(const char *filename) { + char resolved[512]; + if (!filename || strstr(filename, "/") || strstr(filename, "..")) + return -1; + + if (payload_mgr_resolve_path(filename, resolved, sizeof(resolved)) != 0) + return -1; + + /* Also remove .json sidecar */ + char meta_path[640]; + snprintf(meta_path, sizeof(meta_path), "%s.json", resolved); + remove(meta_path); + + /* Update autoload config */ + pldmgr_autoload_update_config_entry(filename, NULL); + + return remove(resolved); +} + +int payload_mgr_import_to_storage(const char *filename, const char *temp_path, + const char *install_source, const char *install_source_detail, + char *msg_buf, size_t msg_buf_size) { + if (!filename || strstr(filename, "/") || strstr(filename, "..")) { + snprintf(msg_buf, msg_buf_size, "Invalid filename"); + return -1; + } + + char folder_name[128]; + pldmgr_utils_get_payload_folder_name(filename, folder_name, sizeof(folder_name)); + + char payload_dir[512]; + snprintf(payload_dir, sizeof(payload_dir), "%s/%s", PAYLOADS_STORAGE_DIR, folder_name); + if (ensure_dir_recursive(payload_dir) != 0) { + snprintf(msg_buf, msg_buf_size, "Failed to create directory"); + return -1; + } + + char final_path[640]; + snprintf(final_path, sizeof(final_path), "%s/%s", payload_dir, filename); + + /* Remove old files in the payload directory */ + payload_mgr_remove_old_files(payload_dir, filename); + + if (rename(temp_path, final_path) != 0) { + snprintf(msg_buf, msg_buf_size, "Failed to move file"); + return -1; + } + + /* Write metadata sidecar */ + char details_path[700]; + snprintf(details_path, sizeof(details_path), "%s/%s.json", payload_dir, filename); + write_simple_payload_details_json(filename, details_path, install_source, install_source_detail); + + pldmgr_log("[PLDMGR] Imported payload: %s -> %s\n", filename, final_path); + snprintf(msg_buf, msg_buf_size, "Imported %s", filename); + return 0; +} + +int payload_mgr_check_existing(const char *filename, char *out_json, size_t out_size) { + char resolved[512]; + if (payload_mgr_resolve_path(filename, resolved, sizeof(resolved)) == 0) { + snprintf(out_json, out_size, "{\"exists\":true,\"path\":\"%s\"}", resolved); + } else { + snprintf(out_json, out_size, "{\"exists\":false}"); + } + return 0; +} + +int payload_mgr_write_metadata(const char *payload_path, const char *install_source, + const char *install_source_detail) { + char details_path[700]; + const char *filename = strrchr(payload_path, '/'); + filename = filename ? filename + 1 : payload_path; + snprintf(details_path, sizeof(details_path), "%s.json", payload_path); + return write_simple_payload_details_json(filename, details_path, install_source, install_source_detail); +} + +/* ── USB Check / Move ──────────────────────────────────────── */ + +int payload_mgr_usb_check(const char *usb_path, char *out_json, size_t out_size) { + if (!is_allowed_usb_path(usb_path)) { + snprintf(out_json, out_size, "{\"error\":\"Invalid USB path\"}"); + return -1; + } + + struct stat st; + if (stat(usb_path, &st) != 0) { + snprintf(out_json, out_size, "{\"error\":\"File not found\"}"); + return -1; + } + + const char *filename = strrchr(usb_path, '/'); + filename = filename ? filename + 1 : usb_path; + + char existing_path[512]; + int exists = (payload_mgr_resolve_path(filename, existing_path, sizeof(existing_path)) == 0); + + char name_e[512], path_e[1024]; + pldmgr_json_escape(filename, name_e, sizeof(name_e)); + pldmgr_json_escape(usb_path, path_e, sizeof(path_e)); + + snprintf(out_json, out_size, + "{\"filename\":\"%s\",\"path\":\"%s\",\"size\":%lld,\"exists\":%s}", + name_e, path_e, (long long)st.st_size, exists ? "true" : "false"); + return 0; +} + +int payload_mgr_usb_move(const char *usb_path, int overwrite, char *out_json, + size_t out_size) { + if (!is_allowed_usb_path(usb_path)) { + snprintf(out_json, out_size, "{\"ok\":false,\"message\":\"Invalid USB path\"}"); + return -1; + } + + struct stat st; + if (stat(usb_path, &st) != 0) { + snprintf(out_json, out_size, "{\"ok\":false,\"message\":\"File not found\"}"); + return -1; + } + + const char *filename = strrchr(usb_path, '/'); + filename = filename ? filename + 1 : usb_path; + + char folder_name[128]; + pldmgr_utils_get_payload_folder_name(filename, folder_name, sizeof(folder_name)); + + char payload_dir[512]; + snprintf(payload_dir, sizeof(payload_dir), "%s/%s", PAYLOADS_STORAGE_DIR, folder_name); + ensure_dir_recursive(payload_dir); + + char final_path[640]; + snprintf(final_path, sizeof(final_path), "%s/%s", payload_dir, filename); + + /* Check for existing file */ + if (!overwrite && access(final_path, F_OK) == 0) { + snprintf(out_json, out_size, "{\"ok\":false,\"message\":\"File already exists\"," + "\"exists\":true}"); + return -1; + } + + /* Remove old files if overwriting */ + if (overwrite) { + payload_mgr_remove_old_files(payload_dir, filename); + } + + if (copy_file(usb_path, final_path) != 0) { + snprintf(out_json, out_size, "{\"ok\":false,\"message\":\"Copy failed\"}"); + return -1; + } + + /* Write metadata */ + char details_path[700]; + snprintf(details_path, sizeof(details_path), "%s/%s.json", payload_dir, filename); + write_simple_payload_details_json(filename, details_path, "usb", usb_path); + + pldmgr_log("[PLDMGR] USB payload moved: %s -> %s\n", usb_path, final_path); + snprintf(out_json, out_size, "{\"ok\":true,\"message\":\"Moved successfully\"}"); + return 0; +} diff --git a/src/process_mgr.c b/src/process_mgr.c new file mode 100644 index 0000000..34636a4 --- /dev/null +++ b/src/process_mgr.c @@ -0,0 +1,101 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "process_mgr.h" +#include "json_helpers.h" +#include "pldmgr.h" + +#define MiB(x) ((x) / (1024.0 * 1024)) + +typedef struct app_info { + uint32_t app_id; + uint64_t unknown1; + char title_id[14]; + char unknown2[0x3c]; +} app_info_t; + +extern int sceKernelGetAppInfo(pid_t pid, app_info_t *info); + +static int is_user_daemon(const char *name, uint32_t app_id) { + if (!name) return 0; + + /* Specifically exclude mini-syscore.elf */ + if (strcmp(name, "mini-syscore.elf") == 0) return 0; + + /* Must have app_id == 0000 */ + if (app_id != 0) return 0; + + const char *ext = strrchr(name, '.'); + if (ext && strcasecmp(ext, ".elf") == 0) return 1; + + return 0; +} + +size_t process_list_json(char *buf, size_t max_size) { + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0}; + size_t buf_size = 0; + void *sysctl_buf = NULL; + + JsonListBuilder jb = { buf, max_size, 0, 1 }; + buf[0] = '\0'; + + json_append(&jb, "{\"processes\":[\n"); + + if (sysctl(mib, 4, NULL, &buf_size, NULL, 0) == 0) { + sysctl_buf = malloc(buf_size); + if (sysctl_buf) { + if (sysctl(mib, 4, sysctl_buf, &buf_size, NULL, 0) == 0) { + int count = 0; + for (void *ptr = sysctl_buf; ptr < (sysctl_buf + buf_size);) { + struct kinfo_proc *ki = (struct kinfo_proc*)ptr; + if (ki->ki_structsize == 0) break; + ptr += ki->ki_structsize; + + app_info_t appinfo; + if(sceKernelGetAppInfo(ki->ki_pid, &appinfo)) { + memset(&appinfo, 0, sizeof(appinfo)); + } + + int is_daemon = is_user_daemon(ki->ki_comm, appinfo.app_id); + + char name_e[512]; + pldmgr_json_escape(ki->ki_comm, name_e, sizeof(name_e)); + + double mem_mib = MiB(ki->ki_rssize * PAGE_SIZE); + + if (json_append(&jb, "%s {\"pid\":%d,\"name\":\"%s\",\"memory\":%.1f,\"is_daemon\":%s}", + (count > 0) ? ",\n" : "", + (int)ki->ki_pid, name_e, mem_mib, is_daemon ? "true" : "false") != 0) { + break; + } + count++; + } + } + free(sysctl_buf); + } + } + + json_append(&jb, "\n]}\n"); + return jb.pos; +} + +int process_kill(int pid) { + if (pid <= 0) return -1; /* Prevent killing kernel or init */ + + /* Prevent killing our own process */ + if (pid == getpid()) return -1; + + if (kill(pid, SIGKILL) == 0) { + return 0; + } + return -1; +} diff --git a/src/repository.c b/src/repository.c new file mode 100644 index 0000000..d667aa5 --- /dev/null +++ b/src/repository.c @@ -0,0 +1,676 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "repository.h" +#include "sha256.h" +#include "json_helpers.h" +#include "config.h" +#include "pldmgr.h" +#include "payload_mgr.h" +#include "assets_cacert_pem.h" + +/* ── cURL download helper ──────────────────────────────────── */ + +static size_t write_data(void *ptr, size_t size, size_t nmemb, FILE *stream) { + return fwrite(ptr, size, nmemb, stream); +} + +int download_to_file(const char *url, const char *out_path) { + CURL *curl; + FILE *fp; + CURLcode res = CURLE_FAILED_INIT; + + if (!url || !out_path) { + pldmgr_log("[PLDMGR] download_to_file: missing url or path\n"); + return -1; + } + + fp = fopen(out_path, "wb"); + if (!fp) { + pldmgr_log("[PLDMGR] download_to_file: failed to open %s\n", out_path); + return -1; + } + + curl = curl_easy_init(); + if (curl) { + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); + + /* User agent */ + curl_easy_setopt(curl, CURLOPT_USERAGENT, "pldmgr/1.0"); + + /* Securely verify SSL against embedded CA bundle */ + struct curl_blob blob; + blob.data = (void *)assets_cacert_pem; + blob.len = assets_cacert_pem_len; + blob.flags = CURL_BLOB_COPY; + + curl_easy_setopt(curl, CURLOPT_CAINFO_BLOB, &blob); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); + + /* Allow redirection */ + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + + res = curl_easy_perform(curl); + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + curl_easy_cleanup(curl); + + if (res != CURLE_OK) { + pldmgr_log("[PLDMGR] curl_easy_perform failed: %s url=%s\n", + curl_easy_strerror(res), url); + } else if (http_code != 200) { + pldmgr_log("[PLDMGR] HTTP status %ld for %s\n", http_code, url); + res = CURLE_HTTP_RETURNED_ERROR; + } + } + + fclose(fp); + + if (res != CURLE_OK) { + remove(out_path); + return -1; + } + + return 0; +} + +/* ── JSON parsing ──────────────────────────────────────────── */ + +int parse_repository_payloads(const char *json, RepoPayload **out_items, size_t *out_count) { + const char *p = json; + RepoPayload *items = NULL; + size_t count = 0; + + *out_items = NULL; + *out_count = 0; + + while ((p = strchr(p, '{')) != NULL) { + const char *end = strchr(p, '}'); + RepoPayload item; + RepoPayload *tmp; + if (!end) + break; + + memset(&item, 0, sizeof(item)); + if (json_extract_string(p, end, "name", item.name, sizeof(item.name)) != 0 || + json_extract_string(p, end, "filename", item.filename, sizeof(item.filename)) != 0 || + json_extract_string(p, end, "url", item.url, sizeof(item.url)) != 0) { + p = end + 1; + continue; + } + + json_extract_string(p, end, "source", item.source, sizeof(item.source)); + json_extract_string(p, end, "source_direct", item.source_direct, sizeof(item.source_direct)); + json_extract_string(p, end, "description", item.description, sizeof(item.description)); + json_extract_string(p, end, "last_update", item.last_update, sizeof(item.last_update)); + json_extract_string(p, end, "version", item.version, sizeof(item.version)); + json_extract_string(p, end, "checksum", item.checksum, sizeof(item.checksum)); + + tmp = (RepoPayload *)realloc(items, (count + 1) * sizeof(RepoPayload)); + if (!tmp) { + free(items); + return -1; + } + items = tmp; + items[count] = item; + count++; + p = end + 1; + } + + *out_items = items; + *out_count = count; + return 0; +} + +static int load_cached_repository(RepoPayload **out_items, size_t *out_count) { + char *json = NULL; + size_t size = 0; + int ret; + + if (read_file_text(REPOSITORY_CACHE_PATH, &json, &size) != 0 || !json || size == 0) { + if (json) + free(json); + return -1; + } + + ret = parse_repository_payloads(json, out_items, out_count); + free(json); + return ret; +} + +/* ── Payload metadata sidecar ──────────────────────────────── */ + +int write_payload_details_json(const RepoPayload *item, const char *details_path, + const char *install_source, const char *install_source_detail) { + char name[256], filename[384], url[1400], source[1400], source_direct[1400]; + char description[1400], last_update[128], version[128], checksum[128]; + char downloaded_at[64], i_src[256], i_detail[1400], i_src_name[256]; + char json_buf[8192]; + time_t now = time(NULL); + struct tm tmv; + + memset(&tmv, 0, sizeof(tmv)); + localtime_r(&now, &tmv); + strftime(downloaded_at, sizeof(downloaded_at), "%Y-%m-%dT%H:%M:%S%z", &tmv); + + pldmgr_json_escape(item->name, name, sizeof(name)); + pldmgr_json_escape(item->filename, filename, sizeof(filename)); + pldmgr_json_escape(item->url, url, sizeof(url)); + pldmgr_json_escape(item->source, source, sizeof(source)); + pldmgr_json_escape(item->source_direct, source_direct, sizeof(source_direct)); + pldmgr_json_escape(item->description, description, sizeof(description)); + pldmgr_json_escape(item->last_update, last_update, sizeof(last_update)); + pldmgr_json_escape(item->version, version, sizeof(version)); + pldmgr_json_escape(item->checksum, checksum, sizeof(checksum)); + pldmgr_json_escape(install_source ? install_source : "unknown", i_src, sizeof(i_src)); + pldmgr_json_escape(install_source_detail ? install_source_detail : "", i_detail, sizeof(i_detail)); + pldmgr_json_escape(item->source_name[0] ? item->source_name : "", i_src_name, sizeof(i_src_name)); + + snprintf(json_buf, sizeof(json_buf), + "{\n" + " \"name\": \"%s\",\n" + " \"filename\": \"%s\",\n" + " \"url\": \"%s\",\n" + " \"source\": \"%s\",\n" + " \"source_direct\": \"%s\",\n" + " \"description\": \"%s\",\n" + " \"last_update\": \"%s\",\n" + " \"version\": \"%s\",\n" + " \"checksum\": \"%s\",\n" + " \"downloaded_at\": \"%s\",\n" + " \"install_source\": \"%s\",\n" + " \"install_source_detail\": \"%s\",\n" + " \"source_name\": \"%s\"\n" + "}\n", + name, filename, url, source, source_direct, description, + last_update, version, checksum, downloaded_at, i_src, i_detail, + i_src_name); + + FILE *f = fopen(details_path, "w"); + if (!f) return -1; + fwrite(json_buf, 1, strlen(json_buf), f); + fclose(f); + return 0; +} + +int write_simple_payload_details_json(const char *filename, const char *details_path, + const char *install_source, const char *install_source_detail) { + RepoPayload item; + memset(&item, 0, sizeof(item)); + strncpy(item.name, filename, sizeof(item.name) - 1); + strncpy(item.filename, filename, sizeof(item.filename) - 1); + return write_payload_details_json(&item, details_path, install_source, install_source_detail); +} + +/* ── Repository cache management ───────────────────────────── */ + +int repository_push_json(const char *json, size_t len) { + RepoPayload *items = NULL; + size_t count = 0; + char tmp_path[512]; + time_t now = time(NULL); + + if (!json || len == 0) { + pldmgr_log("[PLDMGR] !!! repository_push: empty body\n"); + return -1; + } + + if (parse_repository_payloads(json, &items, &count) != 0 || count == 0) { + pldmgr_log("[PLDMGR] !!! repository_push: JSON parse failed or 0 entries\n"); + if (items) free(items); + return -1; + } + free(items); + + ensure_dir_recursive(BASE_DATA_DIR); + ensure_dir_recursive(PAYLOADS_STORAGE_DIR); + + snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", REPOSITORY_CACHE_PATH); + if (write_file_text(tmp_path, json, len) != 0) { + remove(tmp_path); + return -1; + } + if (rename(tmp_path, REPOSITORY_CACHE_PATH) != 0) { + remove(tmp_path); + return -1; + } + + config_write_last_update((long)now); + pldmgr_log("[PLDMGR] Repository cache updated via browser push (%zu entries)\n", count); + return 0; +} + +int repository_ensure_fresh(int force_refresh) { + long last_update = 0; + time_t now = time(NULL); + char tmp_path[512]; + char *json = NULL; + size_t json_size = 0; + RepoPayload *items = NULL; + size_t count = 0; + + ensure_dir_recursive(BASE_DATA_DIR); + ensure_dir_recursive(PAYLOADS_STORAGE_DIR); + + config_read_last_update(&last_update); + + if (!force_refresh && access(REPOSITORY_CACHE_PATH, F_OK) == 0) { + long delta = (long)now - last_update; + if (delta >= 0 && delta < REPOSITORY_REFRESH_INTERVAL_SEC) { + return 0; + } + } + + snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", REPOSITORY_CACHE_PATH); + if (download_to_file(REPOSITORY_SOURCE_URL, tmp_path) != 0) { + return -1; + } + + if (read_file_text(tmp_path, &json, &json_size) != 0 || json_size == 0) { + remove(tmp_path); + if (json) + free(json); + return -1; + } + + if (parse_repository_payloads(json, &items, &count) != 0 || count == 0) { + pldmgr_log("[PLDMGR] !!! Repository payload JSON parse failed\n"); + free(items); + free(json); + remove(tmp_path); + return -1; + } + + free(items); + free(json); + + if (rename(tmp_path, REPOSITORY_CACHE_PATH) != 0) { + remove(tmp_path); + return -1; + } + + config_write_last_update((long)now); + pldmgr_log("[PLDMGR] Repository cache refreshed (%ld entries timestamp)\n", (long)count); + return 0; +} + +size_t repository_list_json(char *json_buf, size_t buf_size, int force_refresh) { + char *cached = NULL; + size_t cached_size = 0; + long last_update = 0; + size_t pos = 0; + + if (force_refresh) { + if (repository_ensure_fresh(1) != 0) { + config_read_last_update(&last_update); + snprintf(json_buf, buf_size, + "{\"payloads\":[],\"last_update\":%ld,\"cache_status\":\"error\"}", + last_update); + return strlen(json_buf); + } + } + + config_read_last_update(&last_update); + + if (read_file_text(REPOSITORY_CACHE_PATH, &cached, &cached_size) != 0 || cached_size == 0) { + if (cached) + free(cached); + snprintf(json_buf, buf_size, + "{\"payloads\":[],\"last_update\":%ld,\"cache_status\":\"missing\"}", + last_update); + return strlen(json_buf); + } + + pos += (size_t)snprintf(json_buf + pos, buf_size - pos, "{\"payloads\":"); + if (pos >= buf_size || buf_size - pos <= 64 || cached_size > (buf_size - pos - 64)) { + snprintf(json_buf, buf_size, + "{\"payloads\":[],\"last_update\":%ld,\"cache_status\":\"truncated\"}", + last_update); + free(cached); + return strlen(json_buf); + } + if (pos < buf_size) { + size_t copy_len = cached_size; + memcpy(json_buf + pos, cached, copy_len); + pos += copy_len; + } + + pos += (size_t)snprintf(json_buf + pos, buf_size - pos, + ",\"last_update\":%ld,\"cache_status\":\"ok\",\"repo_url\":\"%s\"}", + last_update, REPOSITORY_SOURCE_URL); + + free(cached); + return pos; +} + +/* ── Install from repository ───────────────────────────────── */ + +int repository_install_download(const char *filename, const char *install_source_detail, + char *msg_buf, size_t msg_buf_size) { + RepoPayload *items = NULL; + size_t count = 0; + int found = -1; + char tmp_path[512]; + + if (msg_buf && msg_buf_size > 0) + msg_buf[0] = '\0'; + + if (!filename || strlen(filename) == 0 || strstr(filename, "/") || strstr(filename, "..")) { + snprintf(msg_buf, msg_buf_size, "Invalid filename"); + return -1; + } + + if (load_cached_repository(&items, &count) != 0 || count == 0) { + if (repository_ensure_fresh(1) == 0) { + if (load_cached_repository(&items, &count) != 0 || count == 0) { + snprintf(msg_buf, msg_buf_size, "Repository cache missing or invalid"); + if (items) free(items); + return -1; + } + } else { + snprintf(msg_buf, msg_buf_size, "Repository cache missing or invalid"); + if (items) free(items); + return -1; + } + } + + for (size_t i = 0; i < count; i++) { + if (strcmp(items[i].filename, filename) == 0) { + found = (int)i; + break; + } + } + + if (found < 0) { + free(items); + snprintf(msg_buf, msg_buf_size, "Payload not found in repository"); + return -1; + } + + ensure_dir_recursive(PAYLOADS_STORAGE_DIR); + snprintf(tmp_path, sizeof(tmp_path), "%s/%s.part", PAYLOADS_STORAGE_DIR, items[found].filename); + + if (download_to_file(items[found].url, tmp_path) != 0) { + free(items); + snprintf(msg_buf, msg_buf_size, "Download failed"); + remove(tmp_path); + return -1; + } + + const char *detail = (install_source_detail && install_source_detail[0]) + ? install_source_detail + : REPOSITORY_SOURCE_URL; + + if (repository_install_commit(items[found].filename, tmp_path, + "repository", detail, + msg_buf, msg_buf_size) != 0) { + free(items); + return -1; + } + + free(items); + return 0; +} + +int repository_install_commit(const char *filename, const char *uploaded_temp_path, + const char *install_source, const char *install_source_detail, + char *msg_buf, size_t msg_buf_size) { + RepoPayload *items = NULL; + size_t count = 0; + int found = -1; + char payload_dir[512]; + char final_path[640]; + char details_path[700]; + + if (!filename || strlen(filename) == 0 || strstr(filename, "/") || strstr(filename, "..")) { + snprintf(msg_buf, msg_buf_size, "Invalid filename"); + return -1; + } + + if (load_cached_repository(&items, &count) != 0 || count == 0) { + if (repository_ensure_fresh(1) == 0) { + if (load_cached_repository(&items, &count) != 0 || count == 0) { + snprintf(msg_buf, msg_buf_size, "Repository cache missing or invalid"); + if (items) free(items); + return -1; + } + } else { + snprintf(msg_buf, msg_buf_size, "Repository cache missing or invalid"); + if (items) free(items); + return -1; + } + } + + if (count == 0) { + snprintf(msg_buf, msg_buf_size, "Repository cache missing or invalid"); + if (items) free(items); + return -1; + } + + for (size_t i = 0; i < count; i++) { + if (strcmp(items[i].filename, filename) == 0) { + found = (int)i; + break; + } + } + + if (found < 0) { + free(items); + snprintf(msg_buf, msg_buf_size, "Payload not found in repository"); + return -1; + } + + char folder_name[128]; + pldmgr_utils_get_payload_folder_name(items[found].filename, folder_name, sizeof(folder_name)); + snprintf(payload_dir, sizeof(payload_dir), "%s/%s", PAYLOADS_STORAGE_DIR, folder_name); + if (ensure_dir_recursive(payload_dir) != 0) { + free(items); + snprintf(msg_buf, msg_buf_size, "Failed to create payload directory"); + return -1; + } + + snprintf(final_path, sizeof(final_path), "%s/%s", payload_dir, items[found].filename); + snprintf(details_path, sizeof(details_path), "%s/%s.json", payload_dir, items[found].filename); + + if (strlen(items[found].checksum) == 64) { + char calculated[65]; + if (compute_sha256_file(uploaded_temp_path, calculated) != 0) { + free(items); + remove(uploaded_temp_path); + snprintf(msg_buf, msg_buf_size, "Checksum computation failed"); + return -1; + } + if (strcasecmp(calculated, items[found].checksum) != 0) { + pldmgr_log("[PLDMGR] !!! Checksum mismatch for %s\n", items[found].filename); + free(items); + remove(uploaded_temp_path); + snprintf(msg_buf, msg_buf_size, "Checksum mismatch"); + return -1; + } + } + + /* Verify succeeded, now clear previous payload and metadata. */ + payload_mgr_remove_old_files(payload_dir, items[found].filename); + + if (rename(uploaded_temp_path, final_path) != 0) { + free(items); + remove(uploaded_temp_path); + snprintf(msg_buf, msg_buf_size, "Failed to finalize payload file"); + return -1; + } + + if (write_payload_details_json(&items[found], details_path, install_source, install_source_detail) != 0) { + free(items); + remove(final_path); + snprintf(msg_buf, msg_buf_size, "Failed to write payload metadata"); + return -1; + } + + pldmgr_log("[PLDMGR] Repository payload installed: %s -> %s\n", items[found].filename, final_path); + snprintf(msg_buf, msg_buf_size, "Installed %s", items[found].filename); + free(items); + return 0; +} + +/* ── Self-update ───────────────────────────────────────────── */ + +int get_elf_pldmgr_version(const char *path, char *out_version, size_t out_size) { + FILE *f = fopen(path, "rb"); + if (!f) return -1; + + char sig[16]; + sig[0] = 'P'; sig[1] = 'L'; sig[2] = 'D'; sig[3] = 'M'; sig[4] = 'G'; + sig[5] = 'R'; sig[6] = '_'; sig[7] = 'V'; sig[8] = 'E'; sig[9] = 'R'; sig[10] = ':'; sig[11] = '\0'; + size_t sig_len = 11; + + char buffer[8192]; + size_t bytes_read; + int match_idx = 0; + + while ((bytes_read = fread(buffer, 1, sizeof(buffer), f)) > 0) { + for (size_t i = 0; i < bytes_read; i++) { + if (buffer[i] == sig[match_idx]) { + match_idx++; + if (match_idx == (int)sig_len) { + size_t v_idx = 0; + i++; + while (i < bytes_read && buffer[i] != '\0' && v_idx < out_size - 1) { + out_version[v_idx++] = buffer[i++]; + } + if (i < bytes_read && buffer[i] == '\0') { + if (v_idx > 0) { + out_version[v_idx] = '\0'; + fclose(f); + return 0; + } else { + match_idx = 0; + continue; + } + } else { + while (v_idx < out_size - 1) { + int c = fgetc(f); + if (c == EOF || c == '\0') break; + out_version[v_idx++] = (char)c; + } + out_version[v_idx] = '\0'; + if (v_idx > 0) { + fclose(f); + return 0; + } else { + match_idx = 0; + continue; + } + } + } + } else { + match_idx = 0; + if (buffer[i] == sig[0]) match_idx = 1; + } + } + } + fclose(f); + return -1; +} + +static int parse_version_part(const char **p) { + int val = 0; + while (**p >= '0' && **p <= '9') { + val = val * 10 + (**p - '0'); + (*p)++; + } + if (**p == '.') (*p)++; + return val; +} + +static int compare_versions(const char *v1, const char *v2) { + const char *p1 = v1; + const char *p2 = v2; + + int maj1 = parse_version_part(&p1); + int maj2 = parse_version_part(&p2); + if (maj1 != maj2) return maj1 - maj2; + + int min1 = parse_version_part(&p1); + int min2 = parse_version_part(&p2); + if (min1 != min2) return min1 - min2; + + int pat1 = parse_version_part(&p1); + int pat2 = parse_version_part(&p2); + + int res = pat1 - pat2; + pldmgr_log("[PLDMGR] Comparing versions: %s vs %s -> %d\n", v1, v2, res); + return res; +} + +int repository_check_self_update(char *out_path, size_t out_size) { + DIR *dir = opendir(PAYLOADS_STORAGE_DIR); + if (!dir) return -1; + + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + if (entry->d_name[0] == '.') continue; + + /* Check for pldmgr or payload-manager directory (case-insensitive) */ + if (strcasecmp(entry->d_name, "pldmgr") == 0 || strcasecmp(entry->d_name, "payload-manager") == 0) { + char subdir_path[512]; + snprintf(subdir_path, sizeof(subdir_path), "%s/%s", PAYLOADS_STORAGE_DIR, entry->d_name); + + struct stat st; + if (stat(subdir_path, &st) == 0 && S_ISDIR(st.st_mode)) { + DIR *sdir = opendir(subdir_path); + if (sdir) { + struct dirent *sentry; + while ((sentry = readdir(sdir)) != NULL) { + if (sentry->d_name[0] == '.') continue; + + /* Look for .elf or .bin files that match our name */ + if (strstr(sentry->d_name, ".elf") || strstr(sentry->d_name, ".bin")) { + if (strcasestr(sentry->d_name, "pldmgr") || strcasestr(sentry->d_name, "payload-manager")) { + char full_path[512]; + snprintf(full_path, sizeof(full_path), "%s/%s", subdir_path, sentry->d_name); + + /* Verify ELF Magic */ + unsigned char magic[4]; + FILE *ef = fopen(full_path, "rb"); + if (ef) { + if (fread(magic, 1, 4, ef) == 4 && magic[0] == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F') { + fclose(ef); + char version[64]; + if (get_elf_pldmgr_version(full_path, version, sizeof(version)) == 0) { + pldmgr_log("[PLDMGR] Found potential update: %s (v%s)\n", full_path, version); + if (compare_versions(version, MENU_VERSION) > 0) { + strncpy(out_path, full_path, out_size); + closedir(sdir); + closedir(dir); + return 0; + } + } + } else { + fclose(ef); + } + } + } + } + } + closedir(sdir); + } + } + } + } + + closedir(dir); + return -1; +} diff --git a/src/sha256.c b/src/sha256.c new file mode 100644 index 0000000..e973e3e --- /dev/null +++ b/src/sha256.c @@ -0,0 +1,164 @@ +#include +#include +#include "sha256.h" + +static const unsigned int sha256_k[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 +}; + +#define SHA256_ROTR(a, b) (((a) >> (b)) | ((a) << (32 - (b)))) +#define SHA256_CH(x, y, z) (((x) & (y)) ^ (~(x) & (z))) +#define SHA256_MAJ(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#define SHA256_EP0(x) (SHA256_ROTR(x, 2) ^ SHA256_ROTR(x, 13) ^ SHA256_ROTR(x, 22)) +#define SHA256_EP1(x) (SHA256_ROTR(x, 6) ^ SHA256_ROTR(x, 11) ^ SHA256_ROTR(x, 25)) +#define SHA256_SIG0(x) (SHA256_ROTR(x, 7) ^ SHA256_ROTR(x, 18) ^ ((x) >> 3)) +#define SHA256_SIG1(x) (SHA256_ROTR(x, 17) ^ SHA256_ROTR(x, 19) ^ ((x) >> 10)) + +static void sha256_transform(SHA256_CTX *ctx, const unsigned char data[]) { + unsigned int a, b, c, d, e, f, g, h, i, j, t1, t2, m[64]; + + for (i = 0, j = 0; i < 16; i++, j += 4) { + m[i] = ((unsigned int)data[j] << 24) | ((unsigned int)data[j + 1] << 16) | + ((unsigned int)data[j + 2] << 8) | ((unsigned int)data[j + 3]); + } + for (; i < 64; i++) { + m[i] = SHA256_SIG1(m[i - 2]) + m[i - 7] + SHA256_SIG0(m[i - 15]) + m[i - 16]; + } + + a = ctx->state[0]; + b = ctx->state[1]; + c = ctx->state[2]; + d = ctx->state[3]; + e = ctx->state[4]; + f = ctx->state[5]; + g = ctx->state[6]; + h = ctx->state[7]; + + for (i = 0; i < 64; i++) { + t1 = h + SHA256_EP1(e) + SHA256_CH(e, f, g) + sha256_k[i] + m[i]; + t2 = SHA256_EP0(a) + SHA256_MAJ(a, b, c); + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + } + + ctx->state[0] += a; + ctx->state[1] += b; + ctx->state[2] += c; + ctx->state[3] += d; + ctx->state[4] += e; + ctx->state[5] += f; + ctx->state[6] += g; + ctx->state[7] += h; +} + +void sha256_init(SHA256_CTX *ctx) { + ctx->datalen = 0; + ctx->bitlen = 0; + ctx->state[0] = 0x6a09e667; + ctx->state[1] = 0xbb67ae85; + ctx->state[2] = 0x3c6ef372; + ctx->state[3] = 0xa54ff53a; + ctx->state[4] = 0x510e527f; + ctx->state[5] = 0x9b05688c; + ctx->state[6] = 0x1f83d9ab; + ctx->state[7] = 0x5be0cd19; +} + +void sha256_update(SHA256_CTX *ctx, const unsigned char data[], size_t len) { + size_t i; + for (i = 0; i < len; i++) { + ctx->data[ctx->datalen] = data[i]; + ctx->datalen++; + if (ctx->datalen == 64) { + sha256_transform(ctx, ctx->data); + ctx->bitlen += 512; + ctx->datalen = 0; + } + } +} + +void sha256_final(SHA256_CTX *ctx, unsigned char hash[]) { + unsigned int i = ctx->datalen; + + if (ctx->datalen < 56) { + ctx->data[i++] = 0x80; + while (i < 56) { + ctx->data[i++] = 0x00; + } + } else { + ctx->data[i++] = 0x80; + while (i < 64) { + ctx->data[i++] = 0x00; + } + sha256_transform(ctx, ctx->data); + memset(ctx->data, 0, 56); + } + + ctx->bitlen += (unsigned long long)ctx->datalen * 8ULL; + ctx->data[63] = (unsigned char)(ctx->bitlen); + ctx->data[62] = (unsigned char)(ctx->bitlen >> 8); + ctx->data[61] = (unsigned char)(ctx->bitlen >> 16); + ctx->data[60] = (unsigned char)(ctx->bitlen >> 24); + ctx->data[59] = (unsigned char)(ctx->bitlen >> 32); + ctx->data[58] = (unsigned char)(ctx->bitlen >> 40); + ctx->data[57] = (unsigned char)(ctx->bitlen >> 48); + ctx->data[56] = (unsigned char)(ctx->bitlen >> 56); + sha256_transform(ctx, ctx->data); + + for (i = 0; i < 4; i++) { + hash[i] = (ctx->state[0] >> (24 - i * 8)) & 0x000000ff; + hash[i + 4] = (ctx->state[1] >> (24 - i * 8)) & 0x000000ff; + hash[i + 8] = (ctx->state[2] >> (24 - i * 8)) & 0x000000ff; + hash[i + 12] = (ctx->state[3] >> (24 - i * 8)) & 0x000000ff; + hash[i + 16] = (ctx->state[4] >> (24 - i * 8)) & 0x000000ff; + hash[i + 20] = (ctx->state[5] >> (24 - i * 8)) & 0x000000ff; + hash[i + 24] = (ctx->state[6] >> (24 - i * 8)) & 0x000000ff; + hash[i + 28] = (ctx->state[7] >> (24 - i * 8)) & 0x000000ff; + } +} + +int compute_sha256_file(const char *path, char out_hex[65]) { + FILE *f = fopen(path, "rb"); + unsigned char hash[32]; + unsigned char buf[4096]; + size_t n; + SHA256_CTX ctx; + + if (!f) { + return -1; + } + + sha256_init(&ctx); + while ((n = fread(buf, 1, sizeof(buf), f)) > 0) { + sha256_update(&ctx, buf, n); + } + fclose(f); + + sha256_final(&ctx, hash); + for (int i = 0; i < 32; i++) { + snprintf(out_hex + (i * 2), 3, "%02x", hash[i]); + } + out_hex[64] = '\0'; + return 0; +} diff --git a/src/sources.c b/src/sources.c new file mode 100644 index 0000000..76737cd --- /dev/null +++ b/src/sources.c @@ -0,0 +1,615 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "sources.h" +#include "repository.h" +#include "sha256.h" +#include "json_helpers.h" +#include "config.h" +#include "pldmgr.h" +#include "payload_mgr.h" + +/* ── Source entry type ─────────────────────────────────────── */ + +typedef struct { + char id[64]; + char name[256]; + char url[1024]; + int removable; +} SourceEntry; + +/* ── Load / Save sources from SOURCES_CONFIG_PATH ──────────── */ + +static int load_sources(SourceEntry *out, int *out_count) { + char *json = NULL; + size_t size = 0; + int count = 0; + + memset(out, 0, sizeof(SourceEntry) * MAX_SOURCES); + + /* Always start with the default source at index 0 */ + strncpy(out[0].id, "default", sizeof(out[0].id) - 1); + strncpy(out[0].name, "Official Repository", sizeof(out[0].name) - 1); + strncpy(out[0].url, REPOSITORY_SOURCE_URL, sizeof(out[0].url) - 1); + out[0].removable = 0; + count = 1; + + if (read_file_text(SOURCES_CONFIG_PATH, &json, &size) != 0 || !json || size == 0) { + if (json) free(json); + *out_count = count; + return 0; + } + + /* Find "sources": [...] array */ + const char *arr_start = strstr(json, "\"sources\""); + if (!arr_start) { free(json); *out_count = count; return 0; } + + arr_start = strchr(arr_start, '['); + if (!arr_start) { free(json); *out_count = count; return 0; } + + const char *p = arr_start + 1; + while (count < MAX_SOURCES && (p = strchr(p, '{')) != NULL) { + const char *end = strchr(p, '}'); + if (!end) break; + + char id[64] = "", name[256] = "", url[1024] = ""; + char removable_str[8] = ""; + + json_extract_string(p, end, "id", id, sizeof(id)); + json_extract_string(p, end, "name", name, sizeof(name)); + json_extract_string(p, end, "url", url, sizeof(url)); + json_extract_string(p, end, "removable", removable_str, sizeof(removable_str)); + + /* Skip the default entry stored in file — we already added it */ + if (strcmp(id, "default") == 0) { p = end + 1; continue; } + + if (name[0] && url[0] && id[0]) { + strncpy(out[count].id, id, sizeof(out[count].id) - 1); + strncpy(out[count].name, name, sizeof(out[count].name) - 1); + strncpy(out[count].url, url, sizeof(out[count].url) - 1); + out[count].removable = (strcmp(removable_str, "false") != 0); + count++; + } + p = end + 1; + } + + free(json); + *out_count = count; + return 0; +} + +static int save_sources(SourceEntry *sources, int count) { + char buf[16384]; + JsonListBuilder jb = { buf, sizeof(buf), 0, 1 }; + + ensure_dir_recursive(BASE_DATA_DIR); + + json_append(&jb, "{\"sources\":[\n"); + for (int i = 0; i < count; i++) { + char id_e[128], name_e[512], url_e[2048]; + pldmgr_json_escape(sources[i].id, id_e, sizeof(id_e)); + pldmgr_json_escape(sources[i].name, name_e, sizeof(name_e)); + pldmgr_json_escape(sources[i].url, url_e, sizeof(url_e)); + if (json_append(&jb, " {\"id\":\"%s\",\"name\":\"%s\",\"url\":\"%s\",\"removable\":%s}%s\n", + id_e, name_e, url_e, + sources[i].removable ? "true" : "false", + (i < count - 1) ? "," : "") != 0) { + break; + } + } + json_append(&jb, "]}\n"); + + return write_file_text(SOURCES_CONFIG_PATH, buf, jb.pos); +} + +/* ── Public API ────────────────────────────────────────────── */ + +int sources_list_json(char *buf, size_t size) { + SourceEntry sources[MAX_SOURCES] = {0}; + int count = 0; + load_sources(sources, &count); + + JsonListBuilder jb = { buf, size, 0, 1 }; + buf[0] = '\0'; + + json_append(&jb, "{\"sources\":[\n"); + for (int i = 0; i < count; i++) { + char id_e[128], name_e[512], url_e[2048]; + pldmgr_json_escape(sources[i].id, id_e, sizeof(id_e)); + pldmgr_json_escape(sources[i].name, name_e, sizeof(name_e)); + pldmgr_json_escape(sources[i].url, url_e, sizeof(url_e)); + if (json_append(&jb, " {\"id\":\"%s\",\"name\":\"%s\",\"url\":\"%s\",\"removable\":%s}%s\n", + id_e, name_e, url_e, + sources[i].removable ? "true" : "false", + (i < count - 1) ? "," : "") != 0) { + break; + } + } + json_append(&jb, "]}\n"); + return 0; +} + +int sources_save(const char *json, size_t len) { + SourceEntry sources[MAX_SOURCES] = {0}; + int count = 0; + + if (!json || len == 0) return -1; + + /* Always put default at slot 0 */ + strncpy(sources[0].id, "default", sizeof(sources[0].id) - 1); + strncpy(sources[0].name, "Official Repository", sizeof(sources[0].name) - 1); + strncpy(sources[0].url, REPOSITORY_SOURCE_URL, sizeof(sources[0].url) - 1); + sources[0].removable = 0; + count = 1; + + const char *arr_start = strstr(json, "\"sources\""); + if (!arr_start) return -1; + arr_start = strchr(arr_start, '['); + if (!arr_start) return -1; + + const char *p = arr_start + 1; + while (count < MAX_SOURCES && (p = strchr(p, '{')) != NULL) { + const char *end = strchr(p, '}'); + if (!end) break; + + char id[64] = "", name[256] = "", url[1024] = ""; + json_extract_string(p, end, "id", id, sizeof(id)); + json_extract_string(p, end, "name", name, sizeof(name)); + json_extract_string(p, end, "url", url, sizeof(url)); + + if (strcmp(id, "default") == 0) { p = end + 1; continue; } + if (!name[0] || !url[0] || !id[0]) { p = end + 1; continue; } + + strncpy(sources[count].id, id, sizeof(sources[count].id) - 1); + strncpy(sources[count].name, name, sizeof(sources[count].name) - 1); + strncpy(sources[count].url, url, sizeof(sources[count].url) - 1); + sources[count].removable = 1; + count++; + p = end + 1; + } + + return save_sources(sources, count); +} + +int sources_add(const char *url, char *msg_buf, size_t msg_size) { + char tmp_path[512]; + char *json = NULL; + size_t json_size = 0; + RepoPayload *items = NULL; + size_t count = 0; + char source_name[256] = ""; + + if (!url || !url[0]) { + snprintf(msg_buf, msg_size, "URL is empty"); + return -1; + } + + ensure_dir_recursive(BASE_DATA_DIR); + snprintf(tmp_path, sizeof(tmp_path), "%s/source_validate.tmp", BASE_DATA_DIR); + + if (download_to_file(url, tmp_path) != 0) { + snprintf(msg_buf, msg_size, "URL unreachable or download failed"); + remove(tmp_path); + return -1; + } + + if (read_file_text(tmp_path, &json, &json_size) != 0 || !json || json_size == 0) { + snprintf(msg_buf, msg_size, "Failed to read downloaded JSON"); + remove(tmp_path); + if (json) free(json); + return -1; + } + remove(tmp_path); + + /* Read top-level "name" field. + * Only search BEFORE the "payloads" key to avoid picking up individual + * payload items' own "name" fields. */ + { + const char *payloads_key = strstr(json, "\"payloads\""); + if (payloads_key) { + size_t search_len = (size_t)(payloads_key - json); + const char *name_key = NULL; + for (size_t i = 0; i + 6 <= search_len; i++) { + if (memcmp(json + i, "\"name\"", 6) == 0) { + name_key = json + i; + break; + } + } + if (name_key) { + const char *colon = strchr(name_key + 6, ':'); + if (colon && (size_t)(colon - json) < search_len) { + const char *q = colon + 1; + while (*q == ' ' || *q == '\t') q++; + if (*q == '"') { + q++; + size_t pos = 0; + while (*q && *q != '"' && pos < sizeof(source_name) - 1) + source_name[pos++] = *q++; + source_name[pos] = '\0'; + } + } + } + } + /* If still empty, fall back to the hostname portion of the URL */ + if (!source_name[0]) { + const char *host = strstr(url, "://"); + host = host ? host + 3 : url; + size_t pos = 0; + while (*host && *host != '/' && *host != ':' && + pos < sizeof(source_name) - 1) + source_name[pos++] = *host++; + source_name[pos] = '\0'; + } + } + + if (parse_repository_payloads(json, &items, &count) != 0 || count == 0) { + free(json); + if (items) free(items); + snprintf(msg_buf, msg_size, "No valid payloads found in source JSON"); + return -1; + } + free(items); + free(json); + + /* Load current sources and append */ + SourceEntry sources[MAX_SOURCES] = {0}; + int src_count = 0; + load_sources(sources, &src_count); + + if (src_count >= MAX_SOURCES) { + snprintf(msg_buf, msg_size, "Maximum number of sources (%d) reached", MAX_SOURCES); + return -1; + } + + /* Check for duplicate URL */ + for (int i = 0; i < src_count; i++) { + if (strcmp(sources[i].url, url) == 0) { + snprintf(msg_buf, msg_size, "Source already added"); + return -1; + } + } + + /* Assign a unique ID */ + char new_id[64]; + snprintf(new_id, sizeof(new_id), "source_%d", (int)time(NULL)); + + strncpy(sources[src_count].id, new_id, sizeof(sources[src_count].id) - 1); + strncpy(sources[src_count].name, source_name, sizeof(sources[src_count].name) - 1); + strncpy(sources[src_count].url, url, sizeof(sources[src_count].url) - 1); + sources[src_count].removable = 1; + src_count++; + + if (save_sources(sources, src_count) != 0) { + snprintf(msg_buf, msg_size, "Failed to save sources"); + return -1; + } + + /* Return the source name in msg_buf for the caller to forward to the UI */ + snprintf(msg_buf, msg_size, "%s", source_name); + return 0; +} + +int sources_remove(int index, char *msg_buf, size_t msg_size) { + if (index <= 0) { + snprintf(msg_buf, msg_size, "Cannot remove the default source"); + return -1; + } + + SourceEntry sources[MAX_SOURCES] = {0}; + int count = 0; + load_sources(sources, &count); + + if (index >= count) { + snprintf(msg_buf, msg_size, "Invalid source index"); + return -1; + } + + /* Shift entries down */ + for (int i = index; i < count - 1; i++) { + sources[i] = sources[i + 1]; + } + count--; + + if (save_sources(sources, count) != 0) { + snprintf(msg_buf, msg_size, "Failed to save sources"); + return -1; + } + + snprintf(msg_buf, msg_size, "OK"); + return 0; +} + +/* ── Multi-source repository ───────────────────────────────── */ + +static void get_source_cache_path(int index, char *out, size_t out_size) { + if (index == 0) { + strncpy(out, REPOSITORY_CACHE_PATH, out_size - 1); + out[out_size - 1] = '\0'; + } else { + snprintf(out, out_size, "%s.src%d.json", REPOSITORY_CACHE_PATH, index); + } +} + +static int ensure_source_fresh(const char *url, const char *cache_path, int force_refresh) { + long last_update = 0; + time_t now = time(NULL); + char tmp_path[512]; + char *json = NULL; + size_t json_size = 0; + RepoPayload *items = NULL; + size_t count = 0; + + ensure_dir_recursive(BASE_DATA_DIR); + + /* Read last update time from the cache file's companion .ts file */ + char ts_path[512]; + snprintf(ts_path, sizeof(ts_path), "%s.ts", cache_path); + FILE *tf = fopen(ts_path, "r"); + if (tf) { + fscanf(tf, "%ld", &last_update); + fclose(tf); + } + + if (!force_refresh && access(cache_path, F_OK) == 0) { + long delta = (long)now - last_update; + if (delta >= 0 && delta < REPOSITORY_REFRESH_INTERVAL_SEC) { + return 0; + } + } + + snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", cache_path); + if (download_to_file(url, tmp_path) != 0) { + return -1; + } + + if (read_file_text(tmp_path, &json, &json_size) != 0 || json_size == 0) { + if (json) free(json); + remove(tmp_path); + return -1; + } + + /* Validate: must have at least 1 payload */ + if (parse_repository_payloads(json, &items, &count) != 0 || count == 0) { + free(items); + free(json); + remove(tmp_path); + return -1; + } + free(items); + free(json); + + if (rename(tmp_path, cache_path) != 0) { + remove(tmp_path); + return -1; + } + + /* Write timestamp */ + tf = fopen(ts_path, "w"); + if (tf) { + fprintf(tf, "%ld", (long)now); + fclose(tf); + } + + return 0; +} + +size_t sources_multi_repository_list_json(char *buf, size_t size, int force_refresh) { + SourceEntry sources[MAX_SOURCES] = {0}; + int src_count = 0; + load_sources(sources, &src_count); + + JsonListBuilder jb = { buf, size, 0, 1 }; + buf[0] = '\0'; + + json_append(&jb, "{\"sources\":[\n"); + + for (int si = 0; si < src_count; si++) { + char cache_path[512]; + get_source_cache_path(si, cache_path, sizeof(cache_path)); + + int fetch_ok = (ensure_source_fresh(sources[si].url, cache_path, force_refresh) == 0); + + char id_e[128], name_e[512]; + pldmgr_json_escape(sources[si].id, id_e, sizeof(id_e)); + pldmgr_json_escape(sources[si].name, name_e, sizeof(name_e)); + + /* Read last_update timestamp */ + long last_update = 0; + char ts_path[512]; + snprintf(ts_path, sizeof(ts_path), "%s.ts", cache_path); + FILE *tf = fopen(ts_path, "r"); + if (tf) { fscanf(tf, "%ld", &last_update); fclose(tf); } + + if (json_append(&jb, " {\"id\":\"%s\",\"name\":\"%s\",\"last_update\":%ld,\"error\":%s,\"payloads\":[\n", + id_e, name_e, last_update, fetch_ok ? "false" : "true") != 0) { + break; + } + + if (fetch_ok) { + char *cached = NULL; + size_t cached_size = 0; + RepoPayload *items = NULL; + size_t count = 0; + + if (read_file_text(cache_path, &cached, &cached_size) == 0 && cached) { + parse_repository_payloads(cached, &items, &count); + free(cached); + } + + for (size_t pi = 0; pi < count; pi++) { + char name_pe[256], filename_e[512], desc_e[2048]; + char ver_e[128], url_e[2048], src_id_e[128], src_name_e[512]; + + pldmgr_json_escape(items[pi].name, name_pe, sizeof(name_pe)); + pldmgr_json_escape(items[pi].filename, filename_e, sizeof(filename_e)); + pldmgr_json_escape(items[pi].description, desc_e, sizeof(desc_e)); + pldmgr_json_escape(items[pi].version, ver_e, sizeof(ver_e)); + pldmgr_json_escape(items[pi].url, url_e, sizeof(url_e)); + pldmgr_json_escape(sources[si].id, src_id_e, sizeof(src_id_e)); + pldmgr_json_escape(sources[si].name, src_name_e, sizeof(src_name_e)); + + if (json_append(&jb, " {\"name\":\"%s\",\"filename\":\"%s\",\"description\":\"%s\"," + "\"version\":\"%s\",\"url\":\"%s\"," + "\"source_id\":\"%s\",\"source_name\":\"%s\"}%s\n", + name_pe, filename_e, desc_e, ver_e, url_e, + src_id_e, src_name_e, + (pi < count - 1) ? "," : "") != 0) { + break; + } + } + + if (items) free(items); + } + + if (json_append(&jb, " ]}%s\n", (si < src_count - 1) ? "," : "") != 0) { + break; + } + } + + json_append(&jb, "]}\n"); + return jb.pos; +} + +int sources_multi_repository_install(const char *filename, const char *source_id, + const char *repo_url, char *msg, size_t msg_size) { + SourceEntry sources[MAX_SOURCES] = {0}; + int src_count = 0; + load_sources(sources, &src_count); + + /* Find the matching source */ + int si = -1; + for (int i = 0; i < src_count; i++) { + if (strcmp(sources[i].id, source_id) == 0) { si = i; break; } + } + + if (si < 0) { + /* Fall back to matching by URL */ + for (int i = 0; i < src_count; i++) { + if (repo_url && strcmp(sources[i].url, repo_url) == 0) { si = i; break; } + } + } + + if (si < 0) { + /* Last resort: use default */ + si = 0; + } + + char cache_path[512]; + get_source_cache_path(si, cache_path, sizeof(cache_path)); + + /* Ensure cache is fresh */ + if (access(cache_path, F_OK) != 0) { + if (ensure_source_fresh(sources[si].url, cache_path, 1) != 0) { + snprintf(msg, msg_size, "Failed to fetch source repository"); + return -1; + } + } + + /* Load cached payloads for this source */ + char *cached = NULL; + size_t cached_size = 0; + RepoPayload *items = NULL; + size_t count = 0; + + if (read_file_text(cache_path, &cached, &cached_size) != 0 || !cached) { + snprintf(msg, msg_size, "Repository cache unavailable"); + return -1; + } + + parse_repository_payloads(cached, &items, &count); + free(cached); + + /* Find the requested payload */ + int found = -1; + for (size_t i = 0; i < count; i++) { + if (strcmp(items[i].filename, filename) == 0) { found = (int)i; break; } + } + + if (found < 0) { + if (items) free(items); + snprintf(msg, msg_size, "Payload not found in source"); + return -1; + } + + /* Download */ + ensure_dir_recursive(PAYLOADS_STORAGE_DIR); + char tmp_path[512]; + snprintf(tmp_path, sizeof(tmp_path), "%s/%s.part", PAYLOADS_STORAGE_DIR, items[found].filename); + + if (download_to_file(items[found].url, tmp_path) != 0) { + if (items) free(items); + remove(tmp_path); + snprintf(msg, msg_size, "Download failed"); + return -1; + } + + /* Commit */ + const char *detail = (repo_url && repo_url[0]) ? repo_url : sources[si].url; + + char folder_name[128]; + char payload_dir[512]; + char final_path[640]; + char details_path[700]; + + pldmgr_utils_get_payload_folder_name(items[found].filename, folder_name, sizeof(folder_name)); + snprintf(payload_dir, sizeof(payload_dir), "%s/%s", PAYLOADS_STORAGE_DIR, folder_name); + + if (ensure_dir_recursive(payload_dir) != 0) { + if (items) free(items); + remove(tmp_path); + snprintf(msg, msg_size, "Failed to create payload directory"); + return -1; + } + + snprintf(final_path, sizeof(final_path), "%s/%s", payload_dir, items[found].filename); + snprintf(details_path, sizeof(details_path), "%s/%s.json", payload_dir, items[found].filename); + + /* Checksum verification (optional — skip if checksum absent) */ + if (strlen(items[found].checksum) == 64) { + char calculated[65]; + if (compute_sha256_file(tmp_path, calculated) != 0) { + if (items) free(items); + remove(tmp_path); + snprintf(msg, msg_size, "Checksum computation failed"); + return -1; + } + if (strcasecmp(calculated, items[found].checksum) != 0) { + pldmgr_log("[PLDMGR] !!! Checksum mismatch for %s\n", items[found].filename); + if (items) free(items); + remove(tmp_path); + snprintf(msg, msg_size, "Checksum mismatch"); + return -1; + } + } + + payload_mgr_remove_old_files(payload_dir, items[found].filename); + + if (rename(tmp_path, final_path) != 0) { + if (items) free(items); + remove(tmp_path); + snprintf(msg, msg_size, "Failed to finalize payload file"); + return -1; + } + + /* Attach source_id and source_name to the item so metadata captures them */ + strncpy(items[found].source, sources[si].id, sizeof(items[found].source) - 1); + strncpy(items[found].source_direct, sources[si].name, sizeof(items[found].source_direct) - 1); + strncpy(items[found].source_name, sources[si].name, sizeof(items[found].source_name) - 1); + + if (write_payload_details_json(&items[found], details_path, "repository", detail) != 0) { + if (items) free(items); + snprintf(msg, msg_size, "Failed to write payload metadata"); + return -1; + } + + pldmgr_log("[PLDMGR] Multi-source install complete: %s (source: %s)\n", + items[found].filename, sources[si].name); + snprintf(msg, msg_size, "Installed %s", items[found].filename); + if (items) free(items); + return 0; +}