diff --git a/deploy/ARTWORK.md b/deploy/ARTWORK.md new file mode 100644 index 0000000..68c6031 --- /dev/null +++ b/deploy/ARTWORK.md @@ -0,0 +1,30 @@ +# Cover art pipeline + +Discord serves Rich Presence images only from assets uploaded to a +Discord application. There is no URL mode and no automatic official art +for custom presences. One shared app covers every install. + +## One-time maintainer setup + +1. Create an application at https://discord.com/developers/applications. +2. Rich Presence, Art Assets, upload one PNG per game. Asset name must be + the lowercase title ID, e.g. `cusa00740` for Terraria. 512x512 PNG. +3. Copy the Application ID into the repo default (`config/config.json` + `application_id`) so every install inherits it. + +## Where icons live on the PS4 + +`/user/appmeta//icon0.png` (around 50KB). Pull over FTP: + +``` +get /user/appmeta/CUSA00740/icon0.png -> cusa00740.png +``` + +Upload as asset `cusa00740`. + +## Runtime behavior + +The daemon sends `assets: { large_image: "", +large_text: "" }` only when `application_id` is set. Without +it, presence posts with no artwork and logs one notice. Missing assets +for a title also degrade to no image. Nothing crashes on absent art. diff --git a/orbisrpc/daemon.c b/orbisrpc/daemon.c index b9c5f7e..499de87 100644 --- a/orbisrpc/daemon.c +++ b/orbisrpc/daemon.c @@ -174,6 +174,7 @@ int daemon_run(const char *fixed_game_name){ log_close(); return 2; } + if(tr == -3){ log_msg("invalid session; fresh identify"); backoff = base_poll; ws_close(&dc.ws); break; } if(tr != 0){ log_msg("gateway dropped; reconnecting"); break; } for(int i = 0; i < 10 && !s_stop; i++) usleep(100000); diff --git a/orbisrpc/discord.c b/orbisrpc/discord.c index 4d79da3..3a03bca 100644 --- a/orbisrpc/discord.c +++ b/orbisrpc/discord.c @@ -146,7 +146,8 @@ int discord_connect(discord_t *d, const char *token){ /* HELLO (text frame carrying {"op":10,...}) */ char buf[2048]; int op=0,fin=0; int nr=rx_frame(d,buf,sizeof buf,&op,&fin,now+15); - if(nr<=0 || (op!=1 && op!=0)){ + if(nr==-4){ log_msg("no HELLO (timeout)"); ws_close(&d->ws); d->connected=0; return -1; } + if(nr<=0 || op!=1){ log_msg("no HELLO (nr=%d op=%d)",nr,op); ws_close(&d->ws); d->connected=0; return -1; @@ -161,6 +162,11 @@ int discord_connect(discord_t *d, const char *token){ jl_free(h); } if(!d->hb_interval_ms) d->hb_interval_ms=45000; + if(d->hb_interval_ms < 5000){ + log_msg("discord: hb_interval %lldms suspiciously small; clamping to 5000", + (long long)d->hb_interval_ms); + d->hb_interval_ms = 5000; + } if(send_identify(d, token) < 0){ log_msg("discord: identify send failed"); ws_close(&d->ws); d->connected=0; @@ -172,6 +178,7 @@ int discord_connect(discord_t *d, const char *token){ int64_t dl=time(NULL)+20; for(;;){ nr=rx_frame(d,buf,sizeof buf,&op,&fin,dl); + if(nr==-4){ log_msg("no READY after identify (timeout)"); break; } if(nr<=0){ log_msg("no READY after identify (nr=%d)",nr); break; } if(op==8){ if(!fin){ log_msg("fragmented close; reconnecting"); ws_close(&d->ws); d->connected=0; return -1; } @@ -182,7 +189,7 @@ int discord_connect(discord_t *d, const char *token){ return code==4004 ? -2 : -1; } if(op==9){ ws_pong(&d->ws); continue; } - if(op!=1 && op!=0) continue; + if(op!=1) continue; int go = gw_op(buf, (size_t)nr); if(go==11){ d->last_ack=time(NULL); continue; } if(go==0 && is_ready(buf, (size_t)nr)){ @@ -323,10 +330,12 @@ int discord_tick(discord_t *d){ log_msg("gateway: reconnect requested"); d->connected=0; return -1; - case 9: /* INVALID_SESSION */ + case 9: /* INVALID_SESSION: session dead, fresh IDENTIFY needed. + * Distinct code so the caller retries promptly instead of + * doubling into a long backoff. */ log_msg("gateway: invalid session"); d->connected=0; - return -1; + return -3; case 11: d->last_ack=now; break; diff --git a/orbisrpc/discord.h b/orbisrpc/discord.h index b6a7059..b11eaa3 100644 --- a/orbisrpc/discord.h +++ b/orbisrpc/discord.h @@ -24,5 +24,5 @@ int discord_set_presence_ex(discord_t *d, const char *state, const char *name, const char *title_id, const char *application_id, int64_t started_epoch); /* op 3 */ int discord_clear_presence(discord_t *d); /* clear activity, stay online */ -int discord_tick(discord_t *d); /* 0 ok; -1 drop/reconnect; -2 auth-fatal */ +int discord_tick(discord_t *d); /* 0 ok; -1 drop/reconnect; -2 auth-fatal; -3 invalid session (retry promptly) */ #endif diff --git a/orbisrpc/sfo.c b/orbisrpc/sfo.c index a7cff37..f72bbd5 100644 --- a/orbisrpc/sfo.c +++ b/orbisrpc/sfo.c @@ -36,10 +36,15 @@ int sfo_title(const unsigned char *buf, size_t n, char *out, size_t cap){ for(uint32_t i = 0; i < h->count; i++){ size_t ko = (size_t)h->key_off + e[i].key_off; if(ko >= n) continue; + /* keys must be null-terminated INSIDE the buffer: bound the scan */ + size_t kmax = n - ko; + size_t klen = 0; + while(klen < kmax && buf[ko + klen]) klen++; + if(klen >= kmax) continue; /* unterminated key: malformed */ const char *key = (const char *)(buf + ko); /* keys of interest: TITLE first, then language variants TITLE_XX */ int is_title = (!strcmp(key, "TITLE") || - (!strncmp(key, "TITLE_", 6) && strlen(key) == 8)); + (klen == 8 && !memcmp(key, "TITLE_", 6))); if(!is_title) continue; if(e[i].fmt != 0x0004 && e[i].fmt != 0x0204) continue; size_t dlen = e[i].len; diff --git a/plugin/Makefile b/plugin/Makefile index fdda154..0211775 100644 --- a/plugin/Makefile +++ b/plugin/Makefile @@ -20,6 +20,7 @@ INTDIR := $(BUILD_DIR) # Our daemon sources (payload main.c is NOT part of the plugin; plugin.c is the entry) DAEMON_SRC := $(ORBISRPC)/log.c $(ORBISRPC)/cfg.c $(ORBISRPC)/jsonlite.c $(ORBISRPC)/b64.c \ + $(ORBISRPC)/sfo.c \ $(ORBISRPC)/tls.c $(ORBISRPC)/ws.c $(ORBISRPC)/discord.c $(ORBISRPC)/detect.c \ $(ORBISRPC)/daemon.c PLUGIN_SRC := $(SRCDIR)/plugin.c $(SRCDIR)/compat.c diff --git a/scripts/build.sh b/scripts/build.sh index 03cf80d..240b625 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -47,7 +47,7 @@ LDFLAGS="-m elf_x86_64 -pie --eh-frame-hdr -L$SDK/lib $LIBS $SDK/lib/crt1.o --sc export OO_PS4_TOOLCHAIN="$SDK" OUT="$ROOT/build"; mkdir -p "$OUT" echo "=== compiling (CC=$CC LD=$LD SDK=$SDK) ===" -for f in log cfg jsonlite b64 tls ws detect discord daemon compat main; do +for f in log cfg jsonlite b64 sfo tls ws detect discord daemon compat main; do "$CC" $CFLAGS -c -o "$OUT/$f.o" "orbisrpc/$f.c" || fail "compile $f" done echo "=== mbedtls (skip net_sockets/timing: POSIX-only) ===" diff --git a/tests/test_utils.c b/tests/test_utils.c index dfb30ec..63ee8dd 100644 --- a/tests/test_utils.c +++ b/tests/test_utils.c @@ -77,6 +77,12 @@ static void test_sfo(void) { /* tiny output buffer still safe */ assert(sfo_title(sfo, 64, out, 4) == 0); assert(out[3] == 0); + /* unterminated key region: must fail, not read OOB */ + memset(sfo, 0x41, sizeof sfo); + sfo[0]=0x00; sfo[1]='P'; sfo[2]='S'; sfo[3]='F'; + sfo[8]=36; sfo[12]=48; sfo[16]=1; + sfo[20]=0; sfo[22]=0x04; sfo[24]=9; sfo[28]=16; sfo[32]=0; + assert(sfo_title(sfo, 64, out, sizeof out) != 0); } static void test_nametable(void) { char out[64];