Add embedded web server skeleton

This commit is contained in:
Knutwurst committed 2026-06-23 08:12:29 +02:00
1 parent 9e89d3a342
commit 56f54fc1a0
10 files changed
+530

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
.DS_Store
*.log
dist/
build/
node_modules/
gen/
*.elf
__pycache__/
*.pyc
+40
View File
@@ -0,0 +1,40 @@
PS5_HOST ?= ps5
PS5_PORT ?= 9021
PATCHDL_HTTP_PORT ?= 12880
ifdef PS5_PAYLOAD_SDK
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
else
$(error PS5_PAYLOAD_SDK is undefined)
endif
PYTHON ?= python3
BIN := patchdl-ps5.elf
SRCS := src/main.c src/patchdl_assets.c src/patchdl_websrv.c
WEB_ASSETS := web/index.html web/styles.css web/app.js
GEN_SRCS := $(patsubst web/%,gen/web/%.c,$(WEB_ASSETS))
CFLAGS := -g -O2 -Wall -Werror -Isrc -DPATCHDL_HTTP_PORT=$(PATCHDL_HTTP_PORT)
LDADD := -lkernel_sys
LDADD += `$(PS5_PAYLOAD_SDK)/bin/prospero-pkg-config libmicrohttpd --libs`
all: $(BIN)
gen/web:
mkdir -p gen/web
gen/web/%.c: web/% scripts/gen_asset_module.py | gen/web
$(PYTHON) scripts/gen_asset_module.py --path $* $< > $@
$(BIN): $(SRCS) $(GEN_SRCS)
$(CC) $(CFLAGS) -o $@ $^ $(LDADD)
test: $(BIN)
$(PS5_DEPLOY) -h $(PS5_HOST) -p $(PS5_PORT) $^
clean:
rm -rf $(BIN) gen
.PHONY: all clean test
+31
View File
@@ -31,9 +31,40 @@ and `nullfs` mount origins instead of trusting title IDs alone.
## Current Contents
```text
Makefile
PS5 payload build using ps5-payload-sdk and libmicrohttpd.
src/
Embedded web server entrypoint and API stubs.
web/
Static web UI prototype for the future embedded PatchDL web server.
```
Open `web/index.html` directly for the mock UI, or serve `web/` from a local
HTTP server.
## Embedded Web Server
PatchDL is intended to run one self-contained ELF. It does not need a second
`websrv.elf` process. The payload starts its own libmicrohttpd server and serves
the UI from compiled-in assets.
Default port:
```text
http://PS5_IP:12880/
```
The port can be changed at build time:
```sh
make PATCHDL_HTTP_PORT=12881
```
or at runtime by passing a port as the first argument, if the loader supports
argv:
```sh
patchdl-ps5.elf 12881
```
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
import argparse
import mimetypes
from pathlib import Path
def c_string(value):
return value.replace("\\", "\\\\").replace('"', '\\"')
def bytes_as_c_array(path):
data = Path(path).read_bytes()
lines = []
for offset in range(0, len(data), 16):
chunk = data[offset:offset + 16]
lines.append(" " + ", ".join(f"0x{byte:02x}" for byte in chunk))
return "{\n" + ",\n".join(lines) + "\n}"
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--path", required=True)
parser.add_argument("file")
args = parser.parse_args()
mime = mimetypes.guess_type(args.path)[0] or "application/octet-stream"
symbol = (
"patchdl_asset_"
+ "".join(ch if ch.isalnum() else "_" for ch in args.path)
)
print('#include "patchdl_assets.h"')
print()
print(f"static const unsigned char {symbol}[] = {bytes_as_c_array(args.file)};")
print()
print("__attribute__((constructor)) static void")
print(f"register_{symbol}(void) {{")
print(
f' patchdl_asset_register("/{c_string(args.path)}", '
f"(const void*){symbol}, sizeof({symbol}), "
f'"{c_string(mime)}");'
)
print("}")
if __name__ == "__main__":
main()
+37
View File
@@ -0,0 +1,37 @@
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "patchdl_websrv.h"
#ifndef PATCHDL_HTTP_PORT
#define PATCHDL_HTTP_PORT 12880
#endif
int
main(int argc, char** argv) {
unsigned short port = PATCHDL_HTTP_PORT;
if(argc > 1) {
int requested_port = atoi(argv[1]);
if(requested_port > 0 && requested_port <= 65535) {
port = (unsigned short)requested_port;
}
}
signal(SIGPIPE, SIG_IGN);
printf("PatchDL starting web UI on port %u\n", port);
if(patchdl_websrv_start(port)) {
puts("PatchDL web server failed to start");
return 1;
}
while(1) {
sleep(60);
}
return 0;
}
+41
View File
@@ -0,0 +1,41 @@
#include "patchdl_assets.h"
#include <stdlib.h>
#include <string.h>
static patchdl_asset_t* asset_head;
void
patchdl_asset_register(const char* path, const void* data, size_t size,
const char* mime) {
patchdl_asset_t* asset = calloc(1, sizeof(*asset));
if(!asset) {
return;
}
asset->path = path;
asset->data = data;
asset->size = size;
asset->mime = mime;
asset->next = asset_head;
asset_head = asset;
}
const patchdl_asset_t*
patchdl_asset_find(const char* path) {
const patchdl_asset_t* asset = asset_head;
if(!path || !strcmp(path, "/")) {
path = "/index.html";
}
while(asset) {
if(!strcmp(asset->path, path)) {
return asset;
}
asset = asset->next;
}
return 0;
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <stddef.h>
typedef struct patchdl_asset {
const char* path;
const void* data;
size_t size;
const char* mime;
struct patchdl_asset* next;
} patchdl_asset_t;
void patchdl_asset_register(const char* path, const void* data, size_t size,
const char* mime);
const patchdl_asset_t* patchdl_asset_find(const char* path);
+304
View File
@@ -0,0 +1,304 @@
#include "patchdl_websrv.h"
#include "patchdl_assets.h"
#include <microhttpd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static struct MHD_Daemon* web_daemon;
static const char status_json[] =
"{"
"\"firmware\":\"11.60\","
"\"firmware_build\":\"0x11600000\","
"\"dns_guard\":\"Aktiv\","
"\"resolver\":\"Internal allowlist\","
"\"free_space_mb\":64218,"
"\"download_dir\":\"/mnt/usb0/patches\""
"}";
static const char config_json[] =
"{"
"\"default_policy\":\"deny\","
"\"download_dir\":\"/mnt/usb0/patches\","
"\"install_after_download\":false,"
"\"delete_pkg_after_install\":false,"
"\"source_policy\":{"
"\"official\":{\"allow_check\":true,\"allow_download\":true,\"allow_install\":true},"
"\"external\":{\"allow_check\":true,\"allow_download\":true,\"allow_install\":true},"
"\"shadowmount\":{\"allow_check\":true,\"allow_download\":true,\"allow_install\":false},"
"\"unknown\":{\"allow_check\":true,\"allow_download\":false,\"allow_install\":false}"
"},"
"\"cdn_allowlist\":["
"\"sgst.prod.dl.playstation.net\","
"\"gst.prod.dl.playstation.net\","
"\"gs2.ww.prod.dl.playstation.net\""
"]"
"}";
static const char titles_json[] =
"["
"{"
"\"title_id\":\"PPSA01628_00\","
"\"name\":\"Call of Duty Black Ops Cold War\","
"\"content_id\":\"UP0002-PPSA01628_00-CODCWTHEGAME0001\","
"\"installed_version\":\"01.032.000\","
"\"compatible_version\":\"01.041.000\","
"\"latest_version\":\"01.041.000\","
"\"latest_required_fw\":\"11.60\","
"\"source_type\":\"official\","
"\"source_path\":\"/system_ex/app/PPSA01628_00\","
"\"mount_from\":\"/dev/ssd0.system_ex\","
"\"enabled\":true,"
"\"mode\":\"latest_compatible\","
"\"queued\":false,"
"\"status\":\"available\""
"},"
"{"
"\"title_id\":\"PPSA90001_00\","
"\"name\":\"Shadowmounted Test Title\","
"\"content_id\":\"UP0000-PPSA90001_00-SHADOWMOUNT0001\","
"\"installed_version\":\"01.000.000\","
"\"compatible_version\":\"01.006.000\","
"\"latest_version\":\"01.009.000\","
"\"latest_required_fw\":\"12.00\","
"\"source_type\":\"shadowmount\","
"\"source_path\":\"/system_ex/app/PPSA90001_00\","
"\"mount_from\":\"/mnt/usb0/itemzflow/Shadowmounted Test Title\","
"\"enabled\":true,"
"\"mode\":\"download_only\","
"\"queued\":false,"
"\"status\":\"available\""
"},"
"{"
"\"title_id\":\"PPSA08329_00\","
"\"name\":\"Example Future Game\","
"\"content_id\":\"EP0000-PPSA08329_00-FUTUREPATCH00001\","
"\"installed_version\":\"01.000.000\","
"\"compatible_version\":null,"
"\"latest_version\":\"01.012.000\","
"\"latest_required_fw\":\"12.50\","
"\"source_type\":\"unknown\","
"\"source_path\":\"/system_ex/app/PPSA08329_00\","
"\"mount_from\":\"\","
"\"enabled\":false,"
"\"mode\":\"disabled\","
"\"queued\":false,"
"\"status\":\"blocked\""
"}"
"]";
static const char downloads_json[] =
"[]";
static enum MHD_Result
queue_buffer(struct MHD_Connection* conn, unsigned int status,
const char* mime, const void* data, size_t size) {
struct MHD_Response* response;
enum MHD_Result result;
response = MHD_create_response_from_buffer(size, (void*)data,
MHD_RESPMEM_PERSISTENT);
if(!response) {
return MHD_NO;
}
MHD_add_response_header(response, MHD_HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
"*");
MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL,
"no-store");
if(mime) {
MHD_add_response_header(response, MHD_HTTP_HEADER_CONTENT_TYPE, mime);
}
result = MHD_queue_response(conn, status, response);
MHD_destroy_response(response);
return result;
}
static enum MHD_Result
queue_text(struct MHD_Connection* conn, unsigned int status, const char* text) {
return queue_buffer(conn, status, "text/plain; charset=utf-8", text,
strlen(text));
}
static enum MHD_Result
queue_json(struct MHD_Connection* conn, unsigned int status, const char* json) {
return queue_buffer(conn, status, "application/json", json, strlen(json));
}
static enum MHD_Result
queue_asset(struct MHD_Connection* conn, const char* url) {
const patchdl_asset_t* asset = patchdl_asset_find(url);
if(!asset) {
return queue_text(conn, MHD_HTTP_NOT_FOUND, "not found");
}
return queue_buffer(conn, MHD_HTTP_OK, asset->mime, asset->data,
asset->size);
}
static const char*
source_type_for_title(const char* title_id) {
if(!title_id) {
return "unknown";
}
if(!strcmp(title_id, "PPSA90001_00")) {
return "shadowmount";
}
if(!strcmp(title_id, "PPSA08329_00")) {
return "unknown";
}
return "official";
}
static int
parse_title_action(const char* url, char* title_id, size_t title_id_size,
char* action, size_t action_size) {
const char* prefix = "/api/titles/";
const char* start;
const char* slash;
size_t len;
if(strncmp(url, prefix, strlen(prefix))) {
return -1;
}
start = url + strlen(prefix);
slash = strchr(start, '/');
if(!slash || slash == start || !slash[1]) {
return -1;
}
len = slash - start;
if(len >= title_id_size) {
return -1;
}
memcpy(title_id, start, len);
title_id[len] = 0;
len = strlen(slash + 1);
if(len >= action_size) {
return -1;
}
memcpy(action, slash + 1, len + 1);
return 0;
}
static enum MHD_Result
handle_title_action(struct MHD_Connection* conn, const char* url) {
char title_id[32];
char action[24];
const char* source_type;
if(parse_title_action(url, title_id, sizeof(title_id), action,
sizeof(action))) {
return queue_text(conn, MHD_HTTP_NOT_FOUND, "not found");
}
source_type = source_type_for_title(title_id);
if(!strcmp(action, "download")) {
if(!strcmp(source_type, "unknown")) {
return queue_json(conn, MHD_HTTP_FORBIDDEN,
"{\"ok\":false,\"reason\":\"source_unknown\"}");
}
if(!strcmp(source_type, "shadowmount")) {
return queue_json(conn, MHD_HTTP_ACCEPTED,
"{\"ok\":true,\"queued\":true,"
"\"source_type\":\"shadowmount\","
"\"install_allowed\":false}");
}
return queue_json(conn, MHD_HTTP_ACCEPTED,
"{\"ok\":true,\"queued\":true,"
"\"install_allowed\":true}");
}
if(!strcmp(action, "check")) {
return queue_json(conn, MHD_HTTP_ACCEPTED,
"{\"ok\":true,\"queued\":true,\"action\":\"check\"}");
}
return queue_text(conn, MHD_HTTP_NOT_FOUND, "not found");
}
static enum MHD_Result
on_request(void* cls, struct MHD_Connection* conn, const char* url,
const char* method, const char* version, const char* upload_data,
size_t* upload_data_size, void** con_cls) {
(void)cls;
(void)version;
(void)upload_data;
(void)con_cls;
if(!strcmp(method, MHD_HTTP_METHOD_OPTIONS)) {
return queue_text(conn, MHD_HTTP_NO_CONTENT, "");
}
if(!strcmp(method, MHD_HTTP_METHOD_POST)) {
if(*upload_data_size) {
*upload_data_size = 0;
return MHD_YES;
}
if(!strcmp(url, "/api/config")) {
return queue_json(conn, MHD_HTTP_OK, config_json);
}
if(!strncmp(url, "/api/titles/", 12)) {
return handle_title_action(conn, url);
}
return queue_text(conn, MHD_HTTP_NOT_FOUND, "not found");
}
if(strcmp(method, MHD_HTTP_METHOD_GET) &&
strcmp(method, MHD_HTTP_METHOD_HEAD)) {
return queue_text(conn, MHD_HTTP_METHOD_NOT_ALLOWED, "method not allowed");
}
if(!strcmp(url, "/api/status")) {
return queue_json(conn, MHD_HTTP_OK, status_json);
}
if(!strcmp(url, "/api/config")) {
return queue_json(conn, MHD_HTTP_OK, config_json);
}
if(!strcmp(url, "/api/titles")) {
return queue_json(conn, MHD_HTTP_OK, titles_json);
}
if(!strcmp(url, "/api/downloads")) {
return queue_json(conn, MHD_HTTP_OK, downloads_json);
}
return queue_asset(conn, url);
}
int
patchdl_websrv_start(unsigned short port) {
if(web_daemon) {
return 0;
}
web_daemon = MHD_start_daemon(MHD_USE_INTERNAL_POLLING_THREAD |
MHD_USE_THREAD_PER_CONNECTION,
port, NULL, NULL, &on_request, NULL,
MHD_OPTION_END);
return web_daemon ? 0 : -1;
}
void
patchdl_websrv_stop(void) {
if(web_daemon) {
MHD_stop_daemon(web_daemon);
web_daemon = 0;
}
}
+4
View File
@@ -0,0 +1,4 @@
#pragma once
int patchdl_websrv_start(unsigned short port);
void patchdl_websrv_stop(void);
+2
View File
@@ -6,6 +6,8 @@ Open `index.html` directly for the mock UI, or serve this directory from the
ELF web server. The JavaScript first tries the real API and falls back to demo
data when the API is not available.
The embedded ELF server serves this UI on port `12880` by default.
## Expected API
```text