mirror of
https://github.com/itsPLK/ps5-payload-manager.git
synced 2026-10-19 02:19:53 -05:00
refactor: decompose main.c and payload_mgr.c into focused modules
Split two monolithic files (main.c 1538→233 lines, payload_mgr.c 2355→467 lines) into 8 single-responsibility modules: - http_server.c: MHD request router and all route handlers - repository.c: repo cache, download, install/commit, self-update - sources.c: multi-source repository management - config.c: PldmgrConfig struct, read/write, JSON config handler - log_server.c: ring buffer, SSE streaming, pldmgr_log() - json_helpers.c: JsonListBuilder, JSON parser, file I/O utilities - sha256.c: SHA256 hash implementation - process_mgr.c: process listing and kill Introduces PldmgrConfig struct to replace the 8-out-param read_next_config_values() pattern. Renames extracted functions to match their new modules (e.g. repository_install_download, sources_add, process_kill). No HTTP route changes — frontend is unaffected.
This commit is contained in:
1 parent
631e7de0c6
commit
2189ee962d
21 files changed
+3919
-3811
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#ifndef CONFIG_H
|
||||
#define CONFIG_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* 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
|
||||
@@ -0,0 +1,12 @@
|
||||
#ifndef HTTP_SERVER_H
|
||||
#define HTTP_SERVER_H
|
||||
|
||||
#include <microhttpd.h>
|
||||
|
||||
/* 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
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef JSON_HELPERS_H
|
||||
#define JSON_HELPERS_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* 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
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef LOG_SERVER_H
|
||||
#define LOG_SERVER_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <microhttpd.h>
|
||||
|
||||
#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
|
||||
+12
-26
@@ -3,41 +3,27 @@
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/*
|
||||
* 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
|
||||
+1
-2
@@ -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"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#ifndef PROCESS_MGR_H
|
||||
#define PROCESS_MGR_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* 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
|
||||
@@ -0,0 +1,69 @@
|
||||
#ifndef REPOSITORY_H
|
||||
#define REPOSITORY_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* 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
|
||||
@@ -0,0 +1,21 @@
|
||||
#ifndef SHA256_H
|
||||
#define SHA256_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
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
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef SOURCES_H
|
||||
#define SOURCES_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* 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
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
#include <microhttpd.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <ctype.h>
|
||||
#include <sys/stat.h>
|
||||
#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);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <pthread.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
+186
-1490
File diff suppressed because it is too large.
Load diff
+405
-2292
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,101 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/proc.h>
|
||||
#include <sys/user.h>
|
||||
#include <sys/sysctl.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,676 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <ctype.h>
|
||||
#include <time.h>
|
||||
#include <dirent.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <curl/curl.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#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;
|
||||
}
|
||||
+615
@@ -0,0 +1,615 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
Reference in new issue
Block a user