Merge pull request #33 from CyberMask367/main

0.70 test build changes
This commit is contained in:
SirHumza authored and GitHub committed 2026-10-08 21:47:45 +02:00
commit bb59ff3ac7
18 files changed
+9194 -9035

No files matched your search

+6 -2
View File
@@ -11,8 +11,12 @@ __pycache__/
config/config.ps4.json
plugin/build/
tests/test_utils
config/icons/*
!config/icons/logo.png
# Retro cover art. Fetched at runtime from retro-games.cybermask.dpdns.org
# (see the *_art URLs in cfg.c) and cached here locally; nothing under
# orbisrpc/ reads these. 3.7 GB across ~23k files, so kept out of history.
config/images/PS1/
config/images/PS2/
config/images/PSP/
# installer artifacts
*.pkg
+13 -15
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="config/icons/logo.png" width="420" alt="orbisRPC">
<img src="config/images/icons/logo.png" width="420" alt="orbisRPC">
</p>
# orbisRPC — Discord Rich Presence for PS4 (GoldHEN RPC)
@@ -43,7 +43,7 @@ playing to Discord: name, cover art, timer. No PC at runtime.</p>
| | |
|---|---|
| 🎮 **Any game, no lists** | Names resolve from your console's metadata (SFO, app.xml) plus Sony's TMDB — CUSA, PPSA, indies, zero per-game setup. |
| 🎮 **Any games** | Names resolve on your console via Sony's TMDB — CUSA, zero per-game setup. |
| 🕹️ **Homebrew + retro** | Homebrew resolves via pkg-zone; PS1/PS2/PSP classics via a custom list. |
| 🖼️ **Real cover art** | Game art served per title, PlayStation logo when idle. |
| ⏱️ **True timers** | Survive reconnects and restarts, resume across quick game switches. |
@@ -78,9 +78,9 @@ Details: [`docs/DAEMON.md`](docs/DAEMON.md) · full index at [`docs/`](docs/)
## Requirements
- Jailbroken PS4 on firmware **9.00 through 13.52**, GoldHEN running.
- Jailbroken PS4 on firmware **from 5.05 upto 13.52**, GoldHEN running.
- A way to send the payload (elfldr on port 9021, or GoldHEN BinLoader
on 9020).
on 9090).
- A Discord account + your user token (see Install).
- Internet on the PS4 that can reach Discord (see Troubleshooting if not).
@@ -117,16 +117,13 @@ test builds change fast — stick to manual runs for now.
**Official release:** will ship a PKG installer that sets everything up
for you — including autorun and the nanoDNS exception fix.
✅ Firmware: confirmed working on 9.00 through 13.52.
✅ Firmware: confirmed working on 9.00 through 13.52 but older fimwares should work.
### What's in the latest test build (0.60)
### What's in the latest test build (0.70)
- Fixed firmware string parsing — older firmware numbers like 9.00 now
show properly.
- Homebrew apps resolve via pkg-zone.
- Retro games (PS1/PS2/PSP) resolve via a custom list.
- Optional presence flags, all `true` by default:
`show_firmware`, `show_idle`, `show_media`, `show_homebrew`.
- Increased ws gateway frame buffer cap to 32MB - should fix oversized gateway frame skipping issue for some users (hopefully🤞).
- fixed minor issue with frame draining.
- added notification for invalid token.
## What you'll see
@@ -273,9 +270,10 @@ Build/test/CI details: [`docs/BUILDING.md`](docs/BUILDING.md).
## Credits
- **SirHumza** — project founder, daemon core.
- **CyberMask367** — firmware coverage to 13.52, pkg-zone homebrew,
retro lists, presence flags, tester wrangling.
- Testers in `testers-chat` — every log file that made a fix possible.
- **CyberMask367** — firmware coverage to 13.52, homebrew and retro games resolving, presence flags, tester wrangling.
- Testers in `testers-chat` on DIscord — every log file that made a fix possible.
- [*PKG-ZONE*](https://pkg-zone.com) — metadata on ps4 hombrew apps used for resolving
- [*PSX DATA CENTER*](https://psxdatacenter.com) — source used to make custom json list in [`config`](config/) for resolving retro games
## License
+1 -8
View File
@@ -1,8 +1 @@
{
"token": "SET_ME",
"application_id": "1536977374795538532",
"art_base_url": "https://raw.githubusercontent.com/SirHumza/orbisrpc-host/main/icons/",
"enabled": 1,
"poll_interval_s": 12,
"presence_state": "On PS4"
}
{"schema_version":1,"token":"SET_ME","application_id":"1536977374795538532","art_base_url":"https://raw.githubusercontent.com/SirHumza/orbisrpc-host/main/icons/","enabled":true,"pkgzone_enabled":true,"retro_enabled":true,"show_firmware":true,"show_idle":true,"show_media":true,"show_homebrew":true,"auto_update":true,"debug":false,"poll_interval_s":12,"presence_state":"On PS4","presence_details_game":"Playing on PlayStation 4","presence_details_home":"Idling on Home Menu","presence_settings_text":"In Settings","asset_idle":"","asset_playing":"","large_art":"https://retro-games.cybermask.dpdns.org/images/ps-logo-full.png","small_art":"https://retro-games.cybermask.dpdns.org/images/ps-logo-blue.png","browser_art":"https://retro-games.cybermask.dpdns.org/images/web_browser.png","home_art":"https://retro-games.cybermask.dpdns.org/images/ps-logo-full.png"}
Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

File renamed without changes.
Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -1,86 +0,0 @@
#!/usr/bin/env python3
"""build_art_table.py - resolve Sony CDN artwork for a title list via TMDB.
RETIRED: per-title tables were replaced by live TMDB + self-learning map.
Kept for history.
Runs on the BUILD host (console network blocks TMDB port 80) and emits
orbisrpc/art_table.h: {titleId, name, icon_url} for every resolvable title.
Also downloads each icon into icons/ for the art_base_url pack.
Usage: python3 docs/research/retired-tables/build_art_table.py titles.txt [--icons icons/]
Title list: one TITLEID per line (CUSAxxxxx etc.); unknown IDs are skipped.
"""
import hashlib
import hmac
import json
import os
import sys
import urllib.request
KEY = bytes([
0xF5, 0xDE, 0x66, 0xD2, 0x68, 0x0E, 0x25, 0x5B, 0x2D, 0xF7, 0x9E, 0x74,
0xF8, 0x90, 0xEB, 0xF3, 0x49, 0x26, 0x2F, 0x61, 0x8B, 0xCA, 0xE2, 0xA9,
0xAC, 0xCD, 0xEE, 0x51, 0x56, 0xCE, 0x8D, 0xF2, 0xCD, 0xF2, 0xD4, 0x8C,
0x71, 0x17, 0x3C, 0xDC, 0x25, 0x94, 0x46, 0x5B, 0x87, 0x40, 0x5D, 0x19,
0x7C, 0xF1, 0xAE, 0xD3, 0xB7, 0xE9, 0x67, 0x1E, 0xEB, 0x56, 0xCA, 0x67,
0x53, 0xC2, 0xE6, 0xB0,
])
TMDB = "http://tmdb.np.dl.playstation.net"
def c_escape(s):
return s.replace("\\", "\\\\").replace('"', '\\"')
def resolve(tid):
mac = hmac.new(KEY, (tid + "_00\x00").encode("latin1")[:12],
hashlib.sha1).hexdigest().upper()
url = f"{TMDB}/tmdb2/{tid}_00_{mac}/{tid}_00.json"
req = urllib.request.Request(url, headers={"User-Agent": "orbisRPC-arttbl"})
d = json.loads(urllib.request.urlopen(req, timeout=20).read())
name = d["names"][0]["name"]
icon = d["icons"][0]["icon"].replace("http://", "https://")
return name, icon
def main():
titles = [l.strip() for l in open(sys.argv[1]) if l.strip()]
icons = sys.argv[3] if len(sys.argv) > 3 and sys.argv[2] == "--icons" else None
if icons:
os.makedirs(icons, exist_ok=True)
rows = []
for tid in titles:
try:
name, icon = resolve(tid)
except Exception as e:
print(f"MISS {tid}: {str(e)[:60]}", flush=True)
continue
print(f"OK {tid} :: {name}", flush=True)
rows.append((tid, name, icon))
if icons:
try:
img = urllib.request.urlopen(icon, timeout=25).read()
open(os.path.join(icons, tid.lower() + ".png"), "wb").write(img)
except Exception as e:
print(f" icon fetch failed: {str(e)[:60]}", flush=True)
with open("orbisrpc/art_table.h", "w") as f:
f.write("/* art_table.h - GENERATED by docs/research/retired-tables/build_art_table.py.\n")
f.write(" * Do not edit: Sony CDN artwork baked at build time so the\n")
f.write(" * console needs no TMDB network access for art.\n")
f.write(f" * Titles: {len(rows)}. */\n")
f.write("#ifndef ORBISRPC_ART_TABLE_H\n#define ORBISRPC_ART_TABLE_H\n")
f.write("#include <stddef.h>\n")
f.write("typedef struct { const char *id; const char *name;"
" const char *icon; } art_entry_t;\n")
f.write("static const art_entry_t ORBISRPC_ART_TABLE[] = {\n")
for tid, name, icon in rows:
f.write(f' {{ "{tid}", "{c_escape(name)}", "{icon}" }},\n')
f.write("};\n#define ORBISRPC_ART_TABLE_N "
f"(sizeof ORBISRPC_ART_TABLE / sizeof ORBISRPC_ART_TABLE[0])\n")
f.write("#endif\n")
print(f"wrote art_table.h with {len(rows)} entries")
if __name__ == "__main__":
main()
@@ -1,69 +0,0 @@
#!/usr/bin/env python3
"""gen_nametable.py - build orbisrpc/nametable.h from config/titles.json.
RETIRED: baked nametables were replaced by the self-learning titles map.
Kept for history.
titles.json maps TITLEID -> display name (extracted from the PS4's
/system_data/priv/mms/app.db tbl_appbrowse_* tables, or extended by hand).
The generated table is compiled in, so name lookup needs no filesystem
access and works inside any console sandbox.
Usage: python3 docs/research/retired-tables/gen_nametable.py
"""
import json
import os
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SRC = os.path.join(ROOT, "config", "titles.json")
DST = os.path.join(ROOT, "orbisrpc", "nametable.h")
def c_escape(s):
out = []
for ch in s:
o = ord(ch)
if ch == '"':
out.append('\\"')
elif ch == "\\":
out.append("\\\\")
elif 32 <= o < 127:
out.append(ch)
else:
# octal: unlike \x, always stops after 3 digits, so a hex
# digit following the escape can never merge into it.
for b in ch.encode("utf-8"):
out.append("\\%03o" % b)
return "".join(out)
def main():
with open(SRC, encoding="utf-8") as fh:
titles = json.load(fh)
items = sorted(titles.items())
with open(DST, "w", encoding="utf-8") as fh:
fh.write("/* nametable.h - GENERATED by docs/research/retired-tables/gen_nametable.py. Do not edit. */\n")
fh.write("#ifndef NAMETABLE_H\n#define NAMETABLE_H\n");
fh.write("#include <stddef.h>\n#include <string.h>\n")
fh.write("typedef struct { const char *id; const char *name; } nt_entry_t;\n")
fh.write("static const nt_entry_t g_nametable[] = {\n")
for tid, name in items:
fh.write(' { "%s", "%s" },\n' % (c_escape(tid), c_escape(name)))
fh.write("};\n")
fh.write("static const size_t g_nametable_count = sizeof g_nametable / sizeof g_nametable[0];\n")
fh.write("static int nametable_lookup(const char *tid, char *out, size_t cap){\n")
fh.write(" if(!tid || !out || cap == 0) return -1;\n")
fh.write(" for(size_t i = 0; i < g_nametable_count; i++){\n")
fh.write(" if(!strcmp(tid, g_nametable[i].id)){\n")
fh.write(" strncpy(out, g_nametable[i].name, cap - 1);\n")
fh.write(" out[cap - 1] = 0;\n")
fh.write(" return out[0] ? 0 : -1;\n")
fh.write(" }\n")
fh.write(" }\n")
fh.write(" return -1;\n")
fh.write("}\n#endif\n")
print("wrote %s with %d entries" % (DST, len(items)))
if __name__ == "__main__":
main()
-37
View File
@@ -1,37 +0,0 @@
APOL00004
CHTM00777
CUSA00021
CUSA00127
CUSA00135
CUSA00411
CUSA00740
CUSA01004
CUSA01015
CUSA01623
CUSA01839
CUSA03245
CUSA04294
CUSA04602
CUSA06014
CUSA06545
CUSA06712
CUSA08992
CUSA09311
CUSA11995
CUSA13323
CUSA14909
CUSA18795
CUSA20177
CUSA20499
CUSA24899
CUSA26618
CUSA33151
FLTZ00003
GBTX00001
LAPY20006
LAPY20009
LAPY20015
MANU90003
NPXS39041
PKGI13337
CUSA09303
+8
View File
@@ -68,6 +68,12 @@ static int s_settings_posted = 0;
static int s_in_browser = -1;
static int s_browser_posted = 0;
/* Shown when the gateway closes with 4004 (token rejected). Same text at
* connect time and mid-session, so one constant covers both. Throttled
* there rather than once-per-boot because a rejected token persists and
* the daemon keeps retrying on purpose -- it never exits on 4004. */
#define TOKEN_REJECTED_MSG "Discord token rejected - make sure Token is valid"
/* "Firmware 13.52" for the Settings presence. Reads through the same helper the
* rest of the daemon uses, so an unavailable firmware string degrades to
* "unknown" instead of stalling the post. */
@@ -349,6 +355,7 @@ int daemon_run(void){
* reload picks up a fixed token on its own. */
log_msg("WARN: token rejected by gateway (close 4004). "
"Fix \"token\" in %s; retrying", CFG_PATH);
notify_throttled("token4004", TOKEN_REJECTED_MSG, 300);
int wait = reconnect_delay(&conn_fails, base_poll, &jctr);
if(sleep_stop(wait)) break;
continue;
@@ -746,6 +753,7 @@ int daemon_run(void){
* 4004 (never strand the daemon). Drop to the outer loop:
* backoff + config reload picks up a fixed token alone. */
log_msg("WARN: token rejected (close 4004). Fix %s; retrying", CFG_PATH);
notify_throttled("token4004", TOKEN_REJECTED_MSG, 300);
ws_close(&dc.ws);
pres_set(&pres, PS_NONE);
if(sleep_stop(reconnect_delay(&conn_fails, base_poll, &jctr))) break;
+9042 -8814
View File
File diff suppressed because it is too large. Load diff
+28 -2
View File
@@ -339,6 +339,10 @@ static int valid_frame_header(const unsigned char *b, uint64_t plen){
return 1;
}
/* ws_skip_plan() lives in ws_skip.c so the host tests can compile it without
* the socket/TLS half of this file. */
#include "ws_skip.c"
int ws_recv_frame(ws_t *w, char *buf, size_t cap, int *opcode_out, int *fin_out){
if(!w || !w->connected || !buf || cap==0) return -1;
for(;;){
@@ -376,7 +380,7 @@ int ws_recv_frame(ws_t *w, char *buf, size_t cap, int *opcode_out, int *fin_out)
size_t h = (hdr < avail) ? hdr : avail;
w->rpos += h; /* eat the header */
size_t payload_here = avail - h;
w->skip_left = plen - (uint64_t)payload_here;
ws_skip_plan(plen, payload_here, &w->skip_left);
w->skip_op = wire_op;
if(w->rpos >= w->rlen){ w->rpos=0; w->rlen=0; }
continue;
@@ -401,7 +405,29 @@ int ws_recv_frame(ws_t *w, char *buf, size_t cap, int *opcode_out, int *fin_out)
return (int)copy;
}
}
if(w->rlen == w->rcap){ /* full with no parseable frame: give up cleanly */
if(w->rlen == w->rcap){
/* A full buffer during a drain is NORMAL, not a fault: an
* oversized frame is by definition larger than the buffer, so
* the drain can only make progress by discarding what is
* buffered. Bailing out here wiped the buffer and returned -2
* with skip_left outstanding, which left the socket mid-frame
* and made the next parse read JSON payload as a frame header
* (observed: op=2 fin=0 plen=125 b1=0x22 -- a quote byte).
* Consume against skip_left instead; the loop below refills
* from TLS, so no headroom is actually needed. */
if(w->skip_left > 0){
size_t take = (size_t)w->skip_left;
if(take > w->rlen) take = w->rlen;
w->skip_left -= take;
w->rpos += take;
if(w->rpos >= w->rlen){ w->rpos=0; w->rlen=0; }
if(w->skip_left == 0){
if(opcode_out)*opcode_out = w->skip_op;
return -3;
}
continue;
}
/* full with no parseable frame and no drain: give up cleanly */
log_msg("ws: buffer full with no frame; dropping %zuB and reconnecting", w->rlen);
w->rpos=0; w->rlen=0; return -2;
}
+16 -1
View File
@@ -7,7 +7,16 @@
#include <stddef.h>
#define WS_RBUF_MIN 65536 /* initial raw socket buffer (READY is big) */
#define WS_RBUF_MAX (8*1024*1024) /* grow limit; bigger frames are skipped */
/* Grow limit; anything larger is drained and skipped.
*
* 32 MB, raised from 8 MB on 2026-10-06: a real account's READY measured
* 11.7 MB across four attempts (11695449 / 11694320 / 11694256 / 11706895),
* well past the old cap, so identify never completed and the gateway
* reported a connect failure. The value is not stable frame-to-frame, so
* this is sized with real headroom rather than matched to one observation.
* rbuf doubles from 64 KB, so a full READY holds ~32 MB of PS4 heap for
* the life of the session. */
#define WS_RBUF_MAX (32*1024*1024)
typedef struct {
int32_t sock; int32_t connected; int32_t fd;
@@ -28,4 +37,10 @@ int ws_send_text(ws_t *w, const char *msg, size_t len);
int ws_recv_frame(ws_t *w, char *buf, size_t cap, int *opcode_out, int *fin_out);
int ws_pong(ws_t *w); /* answer a server PING (call when recv gives opcode 9) */
int ws_close(ws_t *w);
/* How many payload bytes remain to discard after an oversized frame's header
* has been consumed. Saturates at 0 rather than wrapping when payload_here
* already covers plen. Pure, so the host tests can pin it: the drain state
* lives in the ws_t and a bad value is only visible on the console as a
* desynced frame stream. */
void ws_skip_plan(uint64_t plen, size_t payload_here, uint64_t *skip_left_out);
#endif
+23
View File
@@ -0,0 +1,23 @@
/* ws_skip.c - oversized-frame drain arithmetic, split out of ws.c.
*
* This is the only part of ws.c that can be tested on the host: it has no
* socket, no tls_ctx_t and no PS4 headers. The rest of ws_recv_frame() is
* wired to those, so a bug here used to be invisible until a real gateway
* handshake on a real console.
*
* Why it is worth isolating: drain state lives in the ws_t, so a wrong
* value here does not fail loudly. On console 2026-10-06 an 11.7 MB READY
* overflowed the buffer, the drain miscounted, and the next parse read JSON
* payload as a frame header (op=2 fin=0 plen=125 b1=0x22 -- a quote byte),
* which surfaced only as "cannot connect to discord".
*
* Included by ws.c; compiled directly by tests/test_utils.c. Both paths get
* the same code, so the tests cannot drift from the payload.
*/
#include "ws.h"
void ws_skip_plan(uint64_t plen, size_t payload_here, uint64_t *skip_left_out){
if(!skip_left_out) return;
*skip_left_out = ((uint64_t)payload_here >= plen)
? 0 : plen - (uint64_t)payload_here;
}
+1 -1
View File
@@ -15,7 +15,7 @@ to prepare a source icon.
"""
import sys, os, struct
SRC = sys.argv[1] if len(sys.argv) > 1 else 'config/icons/logo.notify.png'
SRC = sys.argv[1] if len(sys.argv) > 1 else 'config/images/icons/logo.notify.png'
OUT = sys.argv[2] if len(sys.argv) > 2 else 'orbisrpc/icon_embedded.h'
MAX_ICON = 512
+56
View File
@@ -16,6 +16,7 @@
#include "../orbisrpc/pkgzone.h"
#include "../orbisrpc/gamecache.h"
#include "../orbisrpc/fw.h"
#include "../orbisrpc/ws.h"
#include "../orbisrpc/gamecache.h"
#include "../orbisrpc/pkgzone.h"
#include "../installer/icfg.h"
@@ -38,6 +39,10 @@ int ws_recv_frame(ws_t *w, char *b, size_t c, int *o, int *f){
}
int ws_pong(ws_t *w){ (void)w; return -1; }
int ws_close(ws_t *w){ (void)w; return -1; }
/* ws.c itself is not linked on the host (it owns a tls_ctx_t), so the pure
* drain arithmetic comes in directly. Only ws_skip_plan is needed, and it is
* real code compiled from the real source, not a copy. */
#include "../orbisrpc/ws_skip.c"
#include <mbedtls/ecdsa.h>
#include <mbedtls/ecp.h>
#include <mbedtls/ctr_drbg.h>
@@ -93,6 +98,56 @@ static void test_gateway_op_spoof(void) {
jl_free(r2);
}
/* --- oversized-frame drain arithmetic ---------------------------------
* A real account's READY measured 11.7 MB (four attempts, 11695449 /
* 11694320 / 11694256 / 11706895), past the old 8 MB cap, so identify never
* completed. The drain then left the socket mid-frame and the next parse
* read JSON payload as a header (op=2 fin=0 plen=125 b1=0x22 -- a quote).
* ws_skip_plan is the pure piece, so the silent failure modes are pinned
* here instead of on the console. */
static void test_ws_skip_plan(void) {
uint64_t left = 0xdeadbeef;
/* The real READY, header just eaten, nothing of the body buffered. */
ws_skip_plan(11695449, 0, &left);
assert(left == 11695449);
/* Part of the body already buffered: drain only the remainder. */
ws_skip_plan(11695449, 65536, &left);
assert(left == 11695449 - 65536);
/* Body fully buffered already -> nothing left to discard. This is the
* saturation case; the old bare subtraction underflowed to ~1.8e19 here
* and the drain never terminated. */
ws_skip_plan(1000, 1000, &left);
assert(left == 0);
/* More buffered than the header claimed: must still saturate, never wrap. */
ws_skip_plan(1000, 5000, &left);
assert(left == 0);
ws_skip_plan(100, (size_t)-1, &left);
assert(left == 0);
/* Draining to exactly zero must land on zero, not one byte over. */
ws_skip_plan(2048, 2047, &left);
assert(left == 1);
ws_skip_plan(2048, 2048, &left);
assert(left == 0);
/* The cap the console actually hits: 11.7 MB frame, 32 MB buffer. */
ws_skip_plan(11695449, 32768, &left);
assert(left == 11695449 - 32768);
/* A frame larger than even the raised cap still drains sanely. */
ws_skip_plan((uint64_t)64 * 1024 * 1024, 0, &left);
assert(left == (uint64_t)64 * 1024 * 1024);
/* NULL out-pointer must not crash. */
ws_skip_plan(1000, 10, NULL);
/* The cap must actually be above the measured READY, or every one of
* these scenarios returns to the console as a failed connect. */
assert(WS_RBUF_MAX > 11706895);
}
static void test_json_oom_safe(void) {
jl_val_t *t = jl_parse("true", 4); assert(t != NULL); jl_free(t);
jl_val_t *f = jl_parse("false", 5); assert(f != NULL); jl_free(f);
@@ -1438,6 +1493,7 @@ int main(void) {
test_procwalk_find_and_pid();
test_json();
test_gateway_op_spoof();
test_ws_skip_plan();
test_json_oom_safe();
test_json_hostile();
test_tmdb();