mirror of
https://github.com/knutwurst/patchdl.git
synced 2026-10-06 08:00:21 +02:00
Add embedded web server skeleton
This commit is contained in:
1 parent
9e89d3a342
commit
56f54fc1a0
10 files changed
+530
No files matched your search
@@ -0,0 +1,9 @@
|
||||
.DS_Store
|
||||
*.log
|
||||
dist/
|
||||
build/
|
||||
node_modules/
|
||||
gen/
|
||||
*.elf
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -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,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
|
||||
```
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
int patchdl_websrv_start(unsigned short port);
|
||||
void patchdl_websrv_stop(void);
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user