From 20c13f1eadb853503c9eb00eac50108d0ad19645 Mon Sep 17 00:00:00 2001 From: SirHumza <204067870+SirHumza@users.noreply.github.com> Date: Fri, 14 Aug 2026 08:43:09 +0200 Subject: [PATCH] feat: PS4RP daemon source + working PS4 build Full daemon source (C, PS4 native): - log/cfg/jsonlite/b64: logging, /data/PS4RP/config.json, JSON, base64 - http.c: OAuth2 token exchange via SceHttp (authorization_code + refresh) - ws.c: WebSocket over SceNet + LibreSSL (SSLv23_client_method), DNS via sceNetResolverStartNtoa, OrbisNetSockaddr packing - discord.c: gateway connect/identify/heartbeat/presence set+clear - detect.c: foreground game via sceUserServiceGetForegroundUser + app.xml/app.db title-name resolution - main.c: poll loop (config poll_interval_s) Build verified on macOS (LLVM 21 + OpenOrbis toolchain v0.5.4): - 9 objects compile, lld 21 links build/ps4rp.elf (169KB) - create-fself produces valid SCE ASLR payload build/ps4rp.fself - libSceLibreSSL exports the OpenSSL-style TLS API used by ws.c --- .gitignore | 9 +- PS4RP/b64.c | 15 ++ PS4RP/b64.h | 7 + PS4RP/cfg.c | 63 ++++++++ PS4RP/cfg.h | 23 +++ PS4RP/detect.c | 131 ++++++++++++++++ PS4RP/detect.h | 9 ++ PS4RP/discord.c | 144 ++++++++++++++++++ PS4RP/discord.h | 22 +++ PS4RP/http.c | 113 ++++++++++++++ PS4RP/http.h | 14 ++ PS4RP/jsonlite.c | 372 ++++++++++++++++++++------------------------- PS4RP/jsonlite.h | 39 +---- PS4RP/log.c | 33 ++++ PS4RP/log.h | 8 + PS4RP/main.c | 99 ++++++++++++ PS4RP/ws.c | 172 +++++++++++++++++++++ PS4RP/ws.h | 13 ++ config/config.json | 11 ++ scripts/build.sh | 33 ++++ 20 files changed, 1087 insertions(+), 243 deletions(-) create mode 100644 PS4RP/b64.c create mode 100644 PS4RP/b64.h create mode 100644 PS4RP/cfg.c create mode 100644 PS4RP/cfg.h create mode 100644 PS4RP/detect.c create mode 100644 PS4RP/detect.h create mode 100644 PS4RP/discord.c create mode 100644 PS4RP/discord.h create mode 100644 PS4RP/http.c create mode 100644 PS4RP/http.h create mode 100644 PS4RP/log.c create mode 100644 PS4RP/log.h create mode 100644 PS4RP/main.c create mode 100644 PS4RP/ws.c create mode 100644 PS4RP/ws.h create mode 100644 config/config.json create mode 100755 scripts/build.sh diff --git a/.gitignore b/.gitignore index 69be6d5..ba7d02c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ -PS4RP/x64/ -PS4RP/*.oelf -*.pkg -pkg.gp4 -eboot.bin +build/ +*.o +*.err +/tmp/ diff --git a/PS4RP/b64.c b/PS4RP/b64.c new file mode 100644 index 0000000..8ac9f2e --- /dev/null +++ b/PS4RP/b64.c @@ -0,0 +1,15 @@ +/* b64.c - standard base64 encoder */ +#include "b64.h" +static const char b64t[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +size_t b64_encode(const unsigned char *in, size_t len, char *out){ + size_t o=0, i; + for(i=0;i>18)&0x3f]; + out[o++]=b64t[(v>>12)&0x3f]; + out[o++]=i+1>6)&0x3f]:'='; + out[o++]=i+2 +size_t b64_encode(const unsigned char *in, size_t len, char *out); +/* returns bytes written to out (no null) */ +#endif \ No newline at end of file diff --git a/PS4RP/cfg.c b/PS4RP/cfg.c new file mode 100644 index 0000000..4854f69 --- /dev/null +++ b/PS4RP/cfg.c @@ -0,0 +1,63 @@ +/* cfg.c - tiny JSON config via jsonlite */ +#include "cfg.h" +#include "jsonlite.h" +#include "log.h" +#include +#include +#include + +cfg_t g_cfg; + +void cfg_defaults(cfg_t *c) { + memset(c, 0, sizeof(*c)); + c->enabled = 1; + c->poll_interval_s = 15; + strncpy(c->client_id, "SET_ME", sizeof(c->client_id)-1); + strncpy(c->presence_state, "Playing", sizeof(c->presence_state)-1); +} + +int cfg_load(const char *path, cfg_t *c) { + cfg_defaults(c); + FILE *f = fopen(path, "rb"); + if (!f) { log_msg("config not found at %s; defaults applied", path); return -1; } + fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); + if (sz <= 0 || sz > 1<<20) { fclose(f); return -1; } + char *buf = (char *)malloc((size_t)sz + 1); + if (!buf) { fclose(f); return -1; } + size_t n = fread(buf, 1, (size_t)sz, f); fclose(f); + buf[n] = 0; + jl_val_t *root = jl_parse(buf, n); + free(buf); + if (!root) { log_msg("config parse failed; using defaults"); return -1; } + const jl_val_t *o; +#define STR(k,f) do { o=jl_obj_get(root,k); if(o&&o->type==JL_STRING) strncpy(c->f,o->str,sizeof(c->f)-1); } while(0) + STR("client_id", client_id); + STR("client_secret", client_secret); + STR("access_token", access_token); + STR("refresh_token", refresh_token); + STR("auth_code", auth_code); + STR("presence_state", presence_state); +#undef STR + o = jl_obj_get(root, "token_expires_at"); if (o && o->type == JL_NUMBER) c->token_expires_at = (int64_t)o->num; + o = jl_obj_get(root, "enabled"); if (o && o->type == JL_BOOL) c->enabled = (int)o->num; + o = jl_obj_get(root, "poll_interval_s"); if (o && o->type == JL_NUMBER) c->poll_interval_s = (int)o->num; + jl_free(root); + return 0; +} + +void cfg_save(const char *path, const cfg_t *c) { + jl_val_t *r = jl_new_object(); + jl_obj_set(r, "client_id", jl_new_string(c->client_id)); + jl_obj_set(r, "client_secret", jl_new_string(c->client_secret)); + jl_obj_set(r, "access_token", jl_new_string(c->access_token)); + jl_obj_set(r, "refresh_token", jl_new_string(c->refresh_token)); + jl_obj_set(r, "token_expires_at", jl_new_number((double)c->token_expires_at)); + jl_obj_set(r, "auth_code", jl_new_string(c->auth_code)); + jl_obj_set(r, "enabled", jl_new_bool(c->enabled)); + jl_obj_set(r, "poll_interval_s", jl_new_number((double)c->poll_interval_s)); + jl_obj_set(r, "presence_state", jl_new_string(c->presence_state)); + char *s = jl_stringify(r); + FILE *f = fopen(path, "wb"); + if (f) { fputs(s, f); fclose(f); } + free(s); jl_free(r); +} \ No newline at end of file diff --git a/PS4RP/cfg.h b/PS4RP/cfg.h new file mode 100644 index 0000000..a1f966f --- /dev/null +++ b/PS4RP/cfg.h @@ -0,0 +1,23 @@ +/* cfg.h - /data/PS4RP/config.json load/save */ +#ifndef CFG_H +#define CFG_H +#include +#define CFG_PATH "/data/PS4RP/config.json" +#define LOG_PATH "/data/PS4RP/log.txt" +#define DATA_DIR "/data/PS4RP" +typedef struct { + char client_id[64]; /* Discord application client id */ + char client_secret[128]; /* only needed to exchange code / refresh */ + char access_token[256]; /* current bearer */ + char refresh_token[256]; /* long-lived refresh */ + int64_t token_expires_at; /* epoch seconds */ + char auth_code[256]; /* pasted authorization code (one-time) */ + int enabled; + int poll_interval_s; /* game-check / presence refresh cadence */ + char presence_state[128]; /* e.g. "In game" or custom */ +} cfg_t; +extern cfg_t g_cfg; +int cfg_load(const char *path, cfg_t *c); +void cfg_save(const char *path, const cfg_t *c); +void cfg_defaults(cfg_t *c); +#endif \ No newline at end of file diff --git a/PS4RP/detect.c b/PS4RP/detect.c new file mode 100644 index 0000000..7f8eaf5 --- /dev/null +++ b/PS4RP/detect.c @@ -0,0 +1,131 @@ +/* detect.c + * Foreground-game detection on PS4. + * - sceUserServiceGetForegroundUser -> user_id >= 0 => a title holds the foreground. + * - We then attempt to name that title: + * a) via app.db tbl_app_static (title_id/title_name) near the foreground row, and + * b) /data/app//app.xml read for the most-recently-modified dir. + * If the title can't be named, we still report active (game is running) and let the + * caller post a generic "On PS4" presence. + * + * NOTE: mapping the foreground *user_id* to that title's pid/titleId is not exposed + * by the public SDK, so the name is best-effort (heuristic). The core signal + * (a game is in the foreground) is reliable. + */ +#include "detect.h" +#include "cfg.h" +#include "log.h" +#include <orbis/UserService.h> +#include <orbis/libkernel.h> +#include <orbis/Sysmodule.h> +#include <orbis/Net.h> +#include <unistd.h> +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <fcntl.h> +#include <dirent.h> + +static int s_user_inited = 0; +static int user_init(void){ + if(s_user_inited) return 0; + /* UserService is an external module -> load via internal id */ + uint32_t r = sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_USER_SERVICE); + if(r != 0){ int32_t ir=(int32_t)r; log_msg("load UserService fail %d", ir); } + int32_t rc = sceUserServiceInitialize(NULL); + (void)rc; + s_user_inited = 1; + return 0; +} + +int detect_foreground_active(void){ + user_init(); + int32_t fg = -1; + int32_t rc = sceUserServiceGetForegroundUser(&fg); + if(rc != 0){ log_msg("GetForegroundUser err %d", rc); return 0; } + return (fg >= 0) ? 1 : 0; +} + +static long scan_recent_titleid(char *out, size_t cap){ + DIR *d = opendir("/data/app"); + if(!d) return -1; + struct dirent *e; long best=-1; out[0]=0; + struct stat st; /* we use d_type where avail */ + char path[256]; size_t plen; + while((e=readdir(d))){ + if(e->d_name[0]=='.') continue; + size_t l=strlen(e->d_name); + if(l!=9 || strncmp(e->d_name,"CUSA",4)!=0) continue; + plen=snprintf(path,sizeof path,"/data/app/%s/app.xml",e->d_name); + if(plen>=sizeof path) continue; + struct stat st2; + if(stat(path,&st2)==0){ + const char *ti = e->d_name; + if(st2.st_mtime > best || best<0){ best=st2.st_mtime; if(cap>1)strncpy(out,ti,cap-1); out[cap-1]=0; } + } + } + closedir(d); + return (out[0])? 0 : -1; +} + +static int appxml_title(const char *titleId, char *out, size_t cap){ + char path[256]; snprintf(path,sizeof path,"/data/app/%s/app.xml",titleId); + int fd=open(path,O_RDONLY); if(fd<0) return -1; + char buf[640]; ssize_t n=read(fd,buf,sizeof buf-1); close(fd); + if(n<=0) return -1; buf[n]=0; + char *p=strstr(buf,"<title>"); + if(!p) p=strstr(buf,"titleName"); + if(!p) return -1; + /* skip to the value */ + p = strchr(p, '>'); + if(!p) return -1; + p++; /* first char of value */ + size_t i=0; + while(*p && *p!='<' && *p!='\n' && *p!='\r' && i<cap-1){ out[i++]=*p++; } + out[i]=0; + return (i>0)?0:-1; +} + +static void appdb_title(const char *titleId, char *out, size_t cap){ + out[0]=0; + const char *dbs[]={ "/system_data/priv/app.db", "/system_data/etc/app.db", NULL }; + for(int k=0;dbs[k];k++){ + FILE *f=fopen(dbs[k],"rb"); + if(!f) continue; + fseek(f,0,SEEK_END); long sz=ftell(f); fseek(f,0,SEEK_SET); + if(sz<=0||sz>1L<<21){ fclose(f); continue; } + char *b=(char*)malloc((size_t)sz+1); + if(!b){fclose(f);return;} + size_t rd=fread(b,1,(size_t)sz,f); fclose(f); b[rd]=0; + /* crude: copy bytes following the titleId as the name until a non-printable */ + const char *q=strstr(b,titleId); + if(q){ + q+=strlen(titleId); /* past the id */ + /* skip until a plausible name starts */ + while(*q && *q!='<'){ + if(*q>='A'&&*q<='z'){ break; } + q++; + } + size_t i=0; + while(*q && *q!='<' && *q!='\n' && *q!='\r' && i<cap-1){ out[i++]=*q++; } + out[i]=0; + free(b); + if(i>0) return; + } + free(b); + } +} + +int detect_current_game(char *out_name, size_t cap, char *out_path, size_t p_cap){ + if(!detect_foreground_active()) return -1; + char titleId[16]=""; int named=0; + if(scan_recent_titleid(titleId,sizeof titleId)==0){ + if(appxml_title(titleId, out_name, cap)==0){ named=1; } + if(!named){ appdb_title(titleId, out_name, cap); named=(out_name[0]!=0); } + if(!named){ strncpy(out_name, titleId, cap-1); out_name[cap-1]=0; } + }else{ + strncpy(out_name, "(unknown game)", cap-1); out_name[cap-1]=0; + } + if(out_path){ snprintf(out_path, p_cap, "/data/PS4RP/.lastgame/%s", titleId[0]?titleId:"unknown"); } + log_msg("detect: active name=%s tid=%s", out_name, titleId); + return 0; +} diff --git a/PS4RP/detect.h b/PS4RP/detect.h new file mode 100644 index 0000000..3088cdb --- /dev/null +++ b/PS4RP/detect.h @@ -0,0 +1,9 @@ +/* detect.h - find current foreground game title + app cache dir. */ +#ifndef DETECT_H +#define DETECT_H +#include <stddef.h> +/* Returns game display name + an asset/cache path the daemon can own. */ +int detect_current_game(char *out_name, size_t cap, char *out_path, size_t p_cap); +/* Returns nonzero if a foreground user app is running */ +int detect_foreground_active(void); +#endif \ No newline at end of file diff --git a/PS4RP/discord.c b/PS4RP/discord.c new file mode 100644 index 0000000..9d74867 --- /dev/null +++ b/PS4RP/discord.c @@ -0,0 +1,144 @@ +/* discord.c - Discord gateway client. */ +#include "discord.h" +#include "http.h" +#include "b64.h" +#include "log.h" +#include "jsonlite.h" +#include <orbis/Net.h> +#include <fcntl.h> +#include <unistd.h> +#include <string.h> +#include <stdlib.h> +#include <time.h> + +static void make_key(char *out){ + unsigned char b[16]; + int fd=open("/dev/urandom",O_RDONLY); + if(fd>=0){ read(fd,b,16); close(fd); } + else { for(int i=0;i<16;i++)b[i]=(unsigned char)(time(NULL)+i*7); } + b64_encode(b,16,out); +} + +int discord_connect(discord_t *d, const char *token, const char *intent){ + (void)intent; + memset(d,0,sizeof(*d)); + strncpy(d->token, token, sizeof d->token-1); + /* fetch gateway url */ + char resp[512]; + if(http_get("https://discord.com/api/gateway/bot",resp,sizeof resp)!=0){ + log_msg("gateway fetch failed; using default"); + } + char url[512]="wss://gateway.discord.gg"; + jl_val_t *r=jl_parse(resp,0); + if(r){ + const jl_val_t *u=jl_obj_get(r,"url"); + if(u&&u->type==JL_STRING){ strncpy(url,u->str,sizeof url-1); url[sizeof url-1]=0; } + jl_free(r); + } + char key[64]=""; make_key(key); + const char *res=(strstr(url,"discord.gg"))? "/?v=10&encoding=json":"/?v=10&encoding=json"; + /* host parse: take after https:// or default gateway.discord.gg */ + const char *host="gateway.discord.gg"; + int port=443; + int rc=ws_connect(&d->ws, host, port, res, key); + if(rc){ log_msg("ws connect fail %d",rc); return rc; } + d->connected=1; d->last_heartbeat=time(NULL); + /* receive HELLO */ + char buf[2048]; int op=0,fin=0; + int nr=ws_recv_frame(&d->ws,buf,sizeof buf,&op,&fin); + if(nr<=0||op!=1){log_msg("no HELLO op=%d nr=%d",op,nr);return -1;} + jl_val_t *h=jl_parse(buf,nr); + if(h){ + const jl_val_t *dd=jl_obj_get(h,"d"); + if(dd){ + const jl_val_t *hi=jl_obj_get(dd,"heartbeat_interval"); + if(hi&&hi->type==JL_NUMBER) d->hb_interval_ms=(int64_t)hi->num; + } + const jl_val_t *s=jl_obj_get(h,"d"); (void)s; + jl_free(h); + } + if(!d->hb_interval_ms) d->hb_interval_ms=45000; + /* IDENTIFY */ + jl_val_t *root=jl_new_object(); + jl_obj_set(root,"op",jl_new_number(2)); + jl_val_t *dd=jl_new_object(); + jl_obj_set(dd,"token",jl_new_string(token)); + jl_obj_set(dd,"intents",jl_new_number(513)); + jl_obj_set(dd,"properties",jl_new_object()); + jl_obj_set(root,"d",dd); + char *s=jl_stringify(root); jl_free(root); + ws_send_text(&d->ws,s,strlen(s)); free(s); + log_msg("discord: gateway connected, hb=%lds", (long)(d->hb_interval_ms/1000)); + return 0; +} + +int discord_set_presence(discord_t *d, const char *state, const char *details, + int party_size, int party_max, + const char *large_image, const char *large_text){ + (void)party_size; (void)party_max; + jl_val_t *dd=jl_new_object(); + jl_val_t *act=jl_new_object(); + jl_obj_set(act,"name",jl_new_string(details?details:"")); + jl_obj_set(act,"type",jl_new_number(0)); /* Playing */ + if(state) jl_obj_set(act,"state",jl_new_string(state)); + jl_val_t *assets=jl_new_object(); + if(large_image) jl_obj_set(assets,"large_image",jl_new_string(large_image)); + if(large_text) jl_obj_set(assets,"large_text", jl_new_string(large_text)); + jl_obj_set(act,"assets",assets); + jl_obj_set(dd,"activities",jl_new_array()); + jl_arr_push(jl_obj_get(dd,"activities"), act); + jl_obj_set(dd,"status","online"); + jl_obj_set(dd,"since",jl_new_number((double)time(NULL))); + jl_obj_set(dd,"afk",jl_new_bool(0)); + jl_val_t *root=jl_new_object(); + jl_obj_set(root,"op",jl_new_number(3)); + jl_obj_set(root,"d",dd); + char *s=jl_stringify(root); jl_free(root); + int r=ws_send_text(&d->ws,s,strlen(s)); free(s); + return (r>=0)?0:-1; +} + +int discord_clear_presence(discord_t *d){ + jl_val_t *dd=jl_new_object(); + jl_obj_set(dd,"activities",jl_new_array()); + jl_obj_set(dd,"status","invisible"); + jl_val_t *root=jl_new_object(); + jl_obj_set(root,"op",jl_new_number(3)); + jl_obj_set(root,"d",dd); + char *s=jl_stringify(root); jl_free(root); + int r=ws_send_text(&d->ws,s,strlen(s)); free(s); + return (r>=0)?0:-1; +} + +int discord_tick(discord_t *d){ + if(!d->connected) return -1; + int64_t now=time(NULL); + if(now - d->last_heartbeat >= d->hb_interval_ms/1000){ + char hb[64]; snprintf(hb,sizeof hb,"{\"op\":1,\"d\":%d}", d->seq); + ws_send_text(&d->ws,hb,strlen(hb)); + d->last_heartbeat=now; + } + /* read any pending server frame */ + char buf[1024]; int op=0,fin=0; + int nr=ws_recv_frame(&d->ws,buf,sizeof buf,&op,&fin); + if(nr<=0){ return -1; } + if(op==0){ /* DISPATCH */ + const char *p=strstr(buf,"\"seq\":"); + if(p){ p+=6; d->seq=(int)strtol(p,NULL,10); } + else if(strstr(buf,"READY")){ /* capture session id */ + jl_val_t *r=jl_parse(buf,nr); + if(r){ + const jl_val_t *dd=jl_obj_get(r,"d"); + if(dd){ const jl_val_t *si=jl_obj_get(dd,"session_id"); + if(si&&si->type==JL_STRING) strncpy(d->session_id,si->str,sizeof d->session_id-1); } + jl_free(r); + } + } + } + return 0; +} + +int discord_reconnect(discord_t *d){ + ws_close(&d->ws); d->connected=0; + return discord_connect(d, d->token, NULL); +} diff --git a/PS4RP/discord.h b/PS4RP/discord.h new file mode 100644 index 0000000..01d89d7 --- /dev/null +++ b/PS4RP/discord.h @@ -0,0 +1,22 @@ +/* discord.h - gateway session + presence. */ +#ifndef DISCORD_H +#define DISCORD_H +#include "ws.h" +typedef struct { + ws_t ws; + int32_t seq; + char session_id[64]; + char token[256]; + int64_t last_heartbeat; + int64_t hb_interval_ms; + int connected; +} discord_t; +int discord_connect(discord_t *d, const char *token, const char *intent); /* intent = "activities" or "rpc.activities" */ +int discord_set_presence(discord_t *d, const char *state, const char *details, + int party_size, int party_max, const char *large_image, + const char *large_text); /* presence on the bot/user */ +int discord_clear_presence(discord_t *d); /* clear activity */ +int discord_tick(discord_t *d); /* heartbeat + keep-alive; returns <0 to reconnect */ +int discord_identify(discord_t *d, const char *token); /* sends IDENTIFY using stored token */ +int discord_reconnect(discord_t *d); +#endif \ No newline at end of file diff --git a/PS4RP/http.c b/PS4RP/http.c new file mode 100644 index 0000000..bd5ada8 --- /dev/null +++ b/PS4RP/http.c @@ -0,0 +1,113 @@ +/* http.c - OAuth2 token endpoint via SceHttp. */ +#include "http.h" +#include "cfg.h" +#include "log.h" +#include <stdbool.h> +#include <orbis/Sysmodule.h> +#include <orbis/Net.h> +#include <orbis/Http.h> +#include <orbis/Ssl.h> +#include <time.h> +#include <string.h> +#include <stdlib.h> + +#define NET_POOL (64 * 1024) +#define SSL_PL (128 * 1024) +#define HTTP_PL (128 * 1024) + +static int s_http_mem = 0, s_ssl_mem = 0, s_http_ctx = 0; /* ids */ + +/* NOTE: SceHttp/SSL headers declare funcs as void() stubs; we call them + * with the real argument layout (no local redeclaration to avoid conflicts). */ +static int http_init(void){ + if(s_http_ctx) return 0; + int r; + uint32_t ur = sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_NET); + r=(int)ur; + if(r<0){log_msg("load NET fail %d",r);return -1;} + if((r=sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_HTTP))<0){log_msg("load HTTP fail %d",r);return -2;} + if((r=sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_SSL))<0){log_msg("load SSL fail %d",r);return -3;} + if((r=sceNetInit())<0){log_msg("sceNetInit %d",r);return -4;} + s_http_mem=(int)sceNetPoolCreate("ps4rpN",NET_POOL,0); if(s_http_mem<0){log_msg("netpool %d",s_http_mem);return -5;} +s_ssl_mem=(int)sceSslInit(SSL_PL); if(s_ssl_mem<0){log_msg("sslinit %d",s_ssl_mem);return -6;} + if((r=sceHttpInit(s_http_mem,s_ssl_mem,HTTP_PL))<0){log_msg("httpinit %d",r);return -7;} + s_http_ctx=r; + return 0; +} + +/* --- json helpers (avoid coupling to cfg's parser) --- */ +static void jstr(const char *js,const char *key,char *out,size_t cap){ + char k[64]; size_t kl=strlen(key); if(kl>=sizeof k){if(out)*out=0;return;} + k[0]='\"'; memcpy(k+1,key,kl); k[1+kl]='\"'; k[2+kl]=0; + const char *p=strstr(js,k); if(!p||!out){if(out)*out=0;return;} + p+=2+kl; while(*p==' '||*p==':')p++; + size_t i=0; + if(*p=='"'){ p++; while(*p&&*p!='"'&&i<cap-1){out[i++]=*p++;} } + else { while(*p&&*p>='0'&&*p<='9'&&i<cap-1){out[i++]=*p++;} } + out[i]=0; +} +static long jnum(const char *js,const char *key){ + char k[64]; size_t kl=strlen(key); if(kl>=sizeof k)return 0; + k[0]='\"'; memcpy(k+1,key,kl); k[1+kl]='\"'; k[2+kl]=0; + const char *p=strstr(js,k); if(!p)return 0; + p+=2+kl; while(*p==' '||*p==':')p++; return strtol(p,NULL,10); +} + +int http_oauth_token(const char *client_id,const char *client_secret, + const char *code,const char *refresh_token, + char *access_token,size_t at_cap, + char *refresh_out,size_t rt_cap, + int64_t *expires_out_epoch){ + if(http_init()) return -10; + int32_t tpl=0,conn=0,req=0,rc=0,status=0; + if(!(client_id&&*client_id)){log_msg("no client_id");return -11;} + char body[1024]; int blen=0; + blen+=snprintf(body+blen,sizeof body-blen,"client_id=%s",client_id?client_id:""); + if(client_secret&&*client_secret) blen+=snprintf(body+blen,sizeof body-blen,"&client_secret=%s",client_secret); + if(code&&*code) blen+=snprintf(body+blen,sizeof body-blen, + "&grant_type=authorization_code&code=%s&redirect_uri=%s", code, "http://localhost:6770/callback"); + else if(refresh_token&&*refresh_token) blen+=snprintf(body+blen,sizeof body-blen, + "&grant_type=refresh_token&refresh_token=%s", refresh_token); + else blen+=snprintf(body+blen,sizeof body-blen,"&grant_type=client_credentials&scope=applications.commands"); + const char *URL="https://discord.com/api/oauth2/token"; + tpl=sceHttpCreateTemplate(s_http_ctx, "PS4RP/1.0", ORBIS_HTTP_VERSION_1_1, 0); if(tpl<0){log_msg("tmpl %d",tpl);rc=-1;goto done;} + conn=sceHttpCreateConnectionWithURL(tpl, URL, 0); if(conn<0){log_msg("conn %d",conn);rc=-2;goto done;} + req=sceHttpCreateRequestWithURL(conn, ORBIS_METHOD_POST, URL, (uint64_t)blen); if(req<0){log_msg("req %d",req);rc=-3;goto done;} + sceHttpAddRequestHeader(req,"Content-Type","application/x-www-form-urlencoded",0); + sceHttpAddRequestHeader(req,"User-Agent","PS4RP/1.0",0); + if((rc=sceHttpSendRequest(req,body,blen))<0){log_msg("send %d",rc);rc=-4;goto done;} + if((rc=sceHttpGetStatusCode(req,&status))<0){log_msg("stat %d",rc);rc=-5;goto done;} + char resp[2048]; int total=0,len; + while((len=sceHttpReadData(req,resp+total,sizeof resp-total))>0){total+=len; if(total>(int)sizeof resp-1)break;} + resp[total>= (int)sizeof resp? (int)sizeof resp-1:total]=0; + jstr(resp,"access_token",access_token,at_cap); + if(refresh_out) jstr(resp,"refresh_token",refresh_out,rt_cap); + long exp=jnum(resp,"expires_in"); + if(expires_out_epoch)*expires_out_epoch=time(NULL)+exp; + log_msg("oauth status=%d at=%s exp=%lds",status,access_token,(long)exp); + rc = (status==200 && access_token && *access_token) ? 0 : status; +done: + if(req>0)sceHttpDeleteRequest(req); + if(conn>0)sceHttpDeleteConnection(conn); + if(tpl>0)sceHttpDeleteTemplate(tpl); + return rc; +} + +int http_get(const char *url,char *out,size_t cap){ + if(http_init()) return -10; + int32_t tpl=0,conn=0,req=0,rc=0; + tpl=sceHttpCreateTemplate(s_http_ctx,"PS4RP/1.0",ORBIS_HTTP_VERSION_1_1,0); if(tpl<0)return -1; + conn=sceHttpCreateConnectionWithURL(tpl,url,0); if(conn<0){rc=-2;goto done;} + req=sceHttpCreateRequestWithURL(conn,ORBIS_METHOD_GET,url,0); if(req<0){rc=-3;goto done;} + sceHttpAddRequestHeader(req,"User-Agent","PS4RP/1.0",0); + if((rc=sceHttpSendRequest(req,0,0))<0){rc=-4;goto done;} + int total=0,len; + while((len=sceHttpReadData(req,out+total,cap-1-total))>0){total+=len; if(total>(int)cap-1)break;} + out[total>= (int)cap? (int)cap-1:total]=0; + rc=0; +done: + if(req>0)sceHttpDeleteRequest(req); + if(conn>0)sceHttpDeleteConnection(conn); + if(tpl>0)sceHttpDeleteTemplate(tpl); + return rc; +} diff --git a/PS4RP/http.h b/PS4RP/http.h new file mode 100644 index 0000000..4fed2e3 --- /dev/null +++ b/PS4RP/http.h @@ -0,0 +1,14 @@ +/* http.h - OAuth token exchange via SceHttp */ +#ifndef HTTP_H +#define HTTP_H +#include <stdint.h> +#include <stddef.h> +/* POST to token endpoint; fills access_token/refresh_token/expires_in. Returns 0 ok. */ +int http_oauth_token(const char *client_id, const char *client_secret, + const char *code, const char *refresh_token, + char *access_token, size_t at_cap, + char *refresh_out, size_t rt_cap, + int64_t *expires_out_epoch); +/* simple GET into caller buffer */ +int http_get(const char *url, char *out, size_t cap); +#endif \ No newline at end of file diff --git a/PS4RP/jsonlite.c b/PS4RP/jsonlite.c index 356982c..6bb4711 100644 --- a/PS4RP/jsonlite.c +++ b/PS4RP/jsonlite.c @@ -1,4 +1,4 @@ -/* jsonlite.c - implementation */ +/* jsonlite.c */ #include "jsonlite.h" #include <stdlib.h> #include <string.h> @@ -6,275 +6,233 @@ #include <ctype.h> static jl_val_t *newval(jl_type_t t) { - jl_val_t *v = (jl_val_t *)calloc(1, sizeof(jl_val_t)); + jl_val_t *v = (jl_val_t*)calloc(1, sizeof(jl_val_t)); if (v) v->type = t; return v; } /* ---- parser ---- */ static void skip_ws(jl_parse_t *p) { - while (p->cur < p->end && (*p->cur == ' ' || *p->cur == '\t' || - *p->cur == '\n' || *p->cur == '\r')) p->cur++; -} - -static char *parse_unicode_escape(const char **sp) { - /* caller ensures 4 hex; returns malloc'd utf8 */ - const char *s = *sp + 2; /* skip 'u' */ - unsigned cp = 0; - for (int i = 0; i < 4; i++) { - char c = s[i]; - cp <<= 4; - if (c >= '0' && c <= '9') cp |= (c - '0'); - else if (c >= 'a' && c <= 'f') cp |= (c - 'a' + 10); - else if (c >= 'A' && c <= 'F') cp |= (c - 'A' + 10); - else return NULL; - } - *sp = s + 4; - char *out = (char *)malloc(5); - int n = 0; - if (cp < 0x80) { out[n++] = (char)cp; } - else if (cp < 0x800) { out[n++]=(char)(0xC0|(cp>>6)); out[n++]=(char)(0x80|(cp&0x3F)); } - else { out[n++]=(char)(0xE0|(cp>>12)); out[n++]=(char)(0x80|((cp>>6)&0x3F)); out[n++]=(char)(0x80|(cp&0x3F)); } - out[n] = 0; - return out; -} - -static jl_val_t *parse_string(jl_parse_t *p) { - if (*p->cur != '"') { p->err = 1; return NULL; } - p->cur++; - jl_val_t *v = newval(JL_STRING); - if (!v) return NULL; - size_t cap = 16, len = 0; - char *buf = (char *)malloc(cap); - while (p->cur < p->end && *p->cur != '"') { - char c = *p->cur; - if (c == '\\') { - p->cur++; - if (p->cur >= p->end) { p->err = 1; free(buf); free(v); return NULL; } - char e = *p->cur++; - switch (e) { - case '"': c = '"'; break; - case '\\': c = '\\'; break; - case '/': c = '/'; break; - case 'b': c = '\b'; break; - case 'f': c = '\f'; break; - case 'n': c = '\n'; break; - case 'r': c = '\r'; break; - case 't': c = '\t'; break; - case 'u': { - char *u = parse_unicode_escape(&p->cur); - if (!u) { p->err = 1; free(buf); free(v); return NULL; } - size_t ul = strlen(u); - if (len + ul + 1 > cap) { cap = (len + ul + 1) * 2; buf = (char *)realloc(buf, cap); } - memcpy(buf + len, u, ul); len += ul; free(u); - continue; - } - default: p->err = 1; free(buf); free(v); return NULL; - } - } - if (len + 2 > cap) { cap *= 2; buf = (char *)realloc(buf, cap); } - buf[len++] = c; - p->cur++; - } - if (p->cur >= p->end) { p->err = 1; free(buf); free(v); return NULL; } /* no closing quote */ - p->cur++; /* skip closing quote */ - buf[len] = 0; - v->str = buf; - v->strlen = len; - return v; + while (p->cur < p->end) { char c = *p->cur; + if (c==' '||c=='\t'||c=='\n'||c=='\r') p->cur++; else break; } } static jl_val_t *parse_value(jl_parse_t *p); -static jl_val_t *parse_array(jl_parse_t *p) { - p->cur++; /* [ */ - jl_val_t *v = newval(JL_ARRAY); - jl_val_t *tail = NULL; - skip_ws(p); - if (p->cur < p->end && *p->cur == ']') { p->cur++; return v; } - while (p->cur < p->end) { - jl_val_t *e = parse_value(p); - if (p->err) { jl_free(v); return NULL; } - if (!v->child) v->child = e; else { tail->next = e; } - tail = e; v->count++; - skip_ws(p); - if (p->cur < p->end && *p->cur == ',') { p->cur++; skip_ws(p); continue; } - if (p->cur < p->end && *p->cur == ']') { p->cur++; return v; } - p->err = 1; jl_free(v); return NULL; +static jl_val_t *parse_string(jl_parse_t *p) { + if (p->cur >= p->end || *p->cur != '"') { p->err=1; return NULL; } + p->cur++; + jl_val_t *v = newval(JL_STRING); if(!v){p->err=1;return NULL;} + size_t cap=32, len=0; + char *buf = (char*)malloc(cap); + while (p->cur < p->end && *p->cur != '"') { + char c = *p->cur++; + if (c=='\\') { + if (p->cur >= p->end){p->err=1;break;} + char e = *p->cur++; + switch(e){ case '"':c='"';break; case '\\':c='\\';break; case '/':c='/';break; + case 'b':c='\b';break; case 'f':c='\f';break; case 'n':c='\n';break; + case 'r':c='\r';break; case 't':c='\t';break; + case 'u':{ /* decode \uXXXX to utf8 */ if (p->cur+4>p->end){p->err=1;break;} + unsigned cp=0; for(int i=0;i<4;i++){char h=*p->cur++; + cp<<=4; if(h>='0'&&h<='9')cp|=(h-'0'); else if(h>='a'&&h<='f')cp|=(h-'a'+10); + else if(h>='A'&&h<='F')cp|=(h-'A'+10); else {p->err=1;break;}} + if(p->err)break; char outb[5]; int on=0; if(cp<0x80)outb[on++]=cp; + else if(cp<0x800){outb[on++]=(char)(0xC0|(cp>>6));outb[on++]=(char)(0x80|(cp&0x3F));} + else {outb[on++]=(char)(0xE0|(cp>>12));outb[on++]=(char)(0x80|((cp>>6)&0x3F));outb[on++]=(char)(0x80|(cp&0x3F));} + if(len+(size_t)on+1>cap){cap=(len+on+1)*2;buf=(char*)realloc(buf,cap);} + memcpy(buf+len,outb,on);len+=on; continue;} + default: p->err=1; break; + } + if(p->err)break; + } + if(len+2>cap){cap*=2;buf=(char*)realloc(buf,cap);} + buf[len++]=c; } - p->err = 1; jl_free(v); return NULL; + if(p->err){free(buf);jl_free(v);return NULL;} + if(p->cur>=p->end){p->err=1;free(buf);jl_free(v);return NULL;} + p->cur++; /* closing quote */ + buf[len]=0; v->str=buf; v->strlen=len; + return v; +} + +static jl_val_t *parse_array(jl_parse_t *p) { + p->cur++; jl_val_t *v=newval(JL_ARRAY); if(!v){p->err=1;return NULL;} + jl_val_t *tail=NULL; + skip_ws(p); + if(p->cur<p->end && *p->cur==']'){p->cur++;return v;} + while(p->cur<p->end){ + jl_val_t *e=parse_value(p); if(p->err){jl_free(v);return NULL;} + if(!v->child)v->child=e;else{tail->next=e;} + tail=e; v->count++; + skip_ws(p); + if(p->cur<p->end&&*p->cur==','){p->cur++;skip_ws(p);continue;} + if(p->cur<p->end&&*p->cur==']'){p->cur++;return v;} + p->err=1;jl_free(v);return NULL; + } + p->err=1;jl_free(v);return NULL; } static jl_val_t *parse_object(jl_parse_t *p) { - p->cur++; /* { */ - jl_val_t *v = newval(JL_OBJECT); - jl_val_t *tail = NULL; + p->cur++; jl_val_t *v=newval(JL_OBJECT); if(!v){p->err=1;return NULL;} + jl_val_t *tail=NULL; skip_ws(p); - if (p->cur < p->end && *p->cur == '}') { p->cur++; return v; } - while (p->cur < p->end) { - jl_val_t *key = parse_string(p); /* uses string parser; key->str is the key */ - if (p->err) { jl_free(v); return NULL; } + if(p->cur<p->end && *p->cur=='}'){p->cur++;return v;} + while(p->cur<p->end){ + jl_val_t *key=parse_string(p); if(p->err){jl_free(v);return NULL;} skip_ws(p); - if (p->cur < p->end && *p->cur == ':') { p->cur++; skip_ws(p); } else { p->err = 1; jl_free(key); jl_free(v); return NULL; } - jl_val_t *val = parse_value(p); - if (p->err) { jl_free(key); jl_free(v); return NULL; } - jl_val_t *pair = newval(JL_OBJECT); /* reuse as pair node: pair->str=key, pair->child=val */ - pair->str = key->str; pair->strlen = key->strlen; - pair->child = val; - key->str = NULL; free(key); key = pair; - if (!v->child) v->child = key; else { tail->next = key; } - tail = key; v->count++; + if(p->cur<p->end&&*p->cur==':'){p->cur++;skip_ws(p);}else{p->err=1;jl_free(key);jl_free(v);return NULL;} + jl_val_t *val=parse_value(p); if(p->err){jl_free(key);jl_free(v);return NULL;} + jl_val_t *pair=newval(JL_OBJECT); pair->str=key->str; pair->strlen=key->strlen; pair->child=val; + free(key); /* moved key->str into pair */ + if(!v->child)v->child=pair;else{tail->next=pair;} + tail=pair; v->count++; skip_ws(p); - if (p->cur < p->end && *p->cur == ',') { p->cur++; skip_ws(p); continue; } - if (p->cur < p->end && *p->cur == '}') { p->cur++; return v; } - p->err = 1; jl_free(v); return NULL; + if(p->cur<p->end&&*p->cur==','){p->cur++;skip_ws(p);continue;} + if(p->cur<p->end&&*p->cur=='}'){p->cur++;return v;} + p->err=1;jl_free(v);return NULL; } - p->err = 1; jl_free(v); return NULL; + p->err=1;jl_free(v);return NULL; } static jl_val_t *parse_value(jl_parse_t *p) { skip_ws(p); - if (p->cur >= p->end) { p->err = 1; return NULL; } - char c = *p->cur; - if (c == '{') return parse_object(p); - if (c == '[') return parse_array(p); - if (c == '"') return parse_string(p); - if (c == 't') { if (p->end - p->cur >= 4 && !memcmp(p->cur,"true",4)) { p->cur += 4; jl_val_t *b=newval(JL_BOOL); b->num=1; return b; } p->err=1; return NULL; } - if (c == 'f') { if (p->end - p->cur >= 5 && !memcmp(p->cur,"false",5)) { p->cur += 5; jl_val_t *b=newval(JL_BOOL); b->num=0; return b; } p->err=1; return NULL; } - if (c == 'n') { if (p->end - p->cur >= 4 && !memcmp(p->cur,"null",4)) { p->cur += 4; return newval(JL_NULL); } p->err=1; return NULL; } - if (c == '-' || (c >= '0' && c <= '9')) { - char *endp; double d = strtod(p->cur, &endp); - if (endp == p->cur) { p->err = 1; return NULL; } - p->cur = endp; - jl_val_t *n = newval(JL_NUMBER); n->num = d; return n; + if(p->cur>=p->end){p->err=1;return NULL;} + char c=*p->cur; + if(c=='{')return parse_object(p); + if(c=='[')return parse_array(p); + if(c=='"')return parse_string(p); + if(c=='t'){ if(p->end-p->cur>=4&&!memcmp(p->cur,"true",4)){p->cur+=4;jl_val_t*b=newval(JL_BOOL);b->num=1;return b;} p->err=1;return NULL;} + if(c=='f'){ if(p->end-p->cur>=5&&!memcmp(p->cur,"false",5)){p->cur+=5;jl_val_t*b=newval(JL_BOOL);b->num=0;return b;} p->err=1;return NULL;} + if(c=='n'){ if(p->end-p->cur>=4&&!memcmp(p->cur,"null",4)){p->cur+=4;return newval(JL_NULL);} p->err=1;return NULL;} + if(c=='-'||(c>='0'&&c<='9')){ + char *ep; double d=strtod(p->cur,&ep); if(ep==p->cur){p->err=1;return NULL;} p->cur=ep; + jl_val_t *n=newval(JL_NUMBER); n->num=d; return n; } - p->err = 1; return NULL; + p->err=1; return NULL; } -jl_val_t *jl_parse(const char *s, size_t len) { - jl_parse_t p = { s, s + (len ? len : strlen(s)), 0 }; - jl_val_t *v = parse_value(&p); - if (p.err) { jl_free(v); return NULL; } +jl_val_t *jl_parse(const char *s, size_t len){ + size_t l = len?len:strlen(s); + jl_parse_t p={s,s+l,0}; + jl_val_t *v=parse_value(&p); + if(p.err){jl_free(v);return NULL;} return v; } -void jl_free(jl_val_t *v) { - if (!v) return; - if (v->type == JL_STRING || v->type == JL_OBJECT) { - free(v->str); - } - /* for object pairs, child==val owned separately, str owned above */ - jl_val_t *c = v->child; - while (c) { - jl_val_t *n = c->next; - if (c->type == JL_OBJECT) { /* pair node: free val(child) + key(str free'd above) */ - if (c->child) jl_free(c->child); - jl_free(c); +void jl_free(jl_val_t *v){ + if(!v)return; + jl_val_t *c=v->child; + if(v->type==JL_STRING){free(v->str);} /* object pair key stored in pair->str */ + while(c){ + jl_val_t *n=c->next; + if(c->type==JL_OBJECT){ /* pair: child=val owns, str=key */ + if(c->child)jl_free(c->child); + free(c->str); } else { - jl_free(c); + if(c->str)free(c->str); + if(c->child)jl_free(c->child); /* shouldn't happen for arrays of scalars */ } - c = n; + free(c); + c=n; } free(v); } +/* Note: jl_free handles OBJECT as pair list (child=val, str=key) and STRING. + Top-level object free: free child pairs properly. */ +/* fix: the walk above frees pair->str and pair->child(val). Good. For ARRAY the child items are standalone (type may be OBJECT-as-pair? no), handle generically above. */ -const jl_val_t *jl_obj_get(const jl_val_t *obj, const char *key) { - if (!obj || obj->type != JL_OBJECT) return NULL; - jl_val_t *p = obj->child; - while (p) { - if (p->type == JL_OBJECT && p->str && !strcmp(p->str, key)) return p->child; - /* also tolerate plain-keyed entries */ - if (p->type != JL_OBJECT && p->str && !strcmp(p->str, key)) return p; - p = p->next; +const jl_val_t *jl_obj_get(const jl_val_t *obj, const char *key){ + if(!obj||obj->type!=JL_OBJECT)return NULL; + jl_val_t *p=obj->child; + while(p){ + if(p->str && !strcmp(p->str,key)){ + /* pair node -> return its value (child), unless value is missing */ + return p->child? p->child : p; + } + p=p->next; } return NULL; } -const jl_val_t *jl_arr_at(const jl_val_t *arr, size_t i) { - if (!arr || arr->type != JL_ARRAY) return NULL; - jl_val_t *e = arr->child; - while (e && i--) e = e->next; +const jl_val_t *jl_arr_at(const jl_val_t *arr, size_t i){ + if(!arr||arr->type!=JL_ARRAY)return NULL; + jl_val_t *e=arr->child; + while(e&&i--){ e=e->next; } return e; } /* ---- serializer ---- */ -static void emit(jl_val_t *v, char **out, size_t *cap, size_t *len); - -static void escstr(const char *s, char **out, size_t *cap, size_t *len) { - size_t l = strlen(s); - size_t need = *len + l * 6 + 2; - if (need > *cap) { *cap = need * 2; *out = (char *)realloc(*out, *cap); } - char *p = *out + *len; *p++ = '"'; - for (size_t i = 0; i < l; i++) { - char c = s[i]; - if (c=='"'||c=='\\'||c=='\n'||c=='\r'||c=='\t') { +static void escstr(const char *s, char **out, size_t *cap, size_t *len){ + size_t l=strlen(s); + size_t need=*len+l*6+2; + if(need>*cap){*cap=need*2;*out=(char*)realloc(*out,*cap);} + char *p=*out+*len; *p++='"'; + for(size_t i=0;i<l;i++){ + char c=s[i]; + if(c=='"'||c=='\\'||(c>=0&&c<0x20)){ *p++='\\'; - switch(c){case '"':*p++='"';break;case '\\':*p++='\\';break;case '\n':*p++='n';break;case '\r':*p++='r';break;case '\t':*p++='t';break;} - } else if ((unsigned char)c < 0x20) { - *p++='\\'; *p++='u'; *p++='0'; *p++='0'; - static const char hx[]="0123456789abcdef"; - *p++=hx[(c>>4)&0xf]; *p++=hx[c&0xf]; + switch(c){case '"':*p++='"';break;case '\\':*p++='\\';break; + case '\n':*p++='n';break;case '\r':*p++='r';break;case '\t':*p++='t';break; + case '\b':*p++='b';break;case '\f':*p++='f';break; + default:{*p++='u';*p++='0';*p++='0';const char hx[]="0123456789abcdef"; + *p++=hx[(c>>4)&0xf];*p++=hx[c&0xf];break;}} } else { *p++=c; } } - *p++='"'; - *len = p - *out; + *p++='"'; *len=p-*out; } -static void emit(jl_val_t *v, char **out, size_t *cap, size_t *len) { - if (!v) return; +static void emit(jl_val_t *v, char **out, size_t *cap, size_t *len){ char buf[64]; - switch (v->type) { - case JL_NULL: { size_t l=strlen("null"); if(*len+l+1>*cap){*cap=(*len+l+2)*2;*out=realloc(*out,*cap);} memcpy(*out+*len,"null",l); *len+=l; break; } - case JL_BOOL: emit_number_fallback: ; - case JL_NUMBER: snprintf(buf,sizeof buf,"%g", v->num); break; + switch(v->type){ + case JL_NULL: { size_t l=4; if(*len+l+1>*cap){*cap=(*len+l+2)*2;*out=realloc(*out,*cap);} memcpy(*out+*len,"null",4);*len+=4; return; } + case JL_BOOL: { const char*s=v->num?"true":"false"; size_t l=strlen(s); + if(*len+l+1>*cap){*cap=(*len+l+2)*2;*out=realloc(*out,*cap);} memcpy(*out+*len,s,l);*len+=l; return; } + case JL_NUMBER: snprintf(buf,sizeof buf,"%.17g",v->num); break; case JL_STRING: escstr(v->str,out,cap,len); return; - case JL_ARRAY: { - size_t l = (*len)+1; if(l>*cap){*cap=l*2;*out=realloc(*out,*cap);} (*out)[(*len)++]='['; - jl_val_t *e=v->child; int first=1; - while(e){ if(!first){ size_t l2=*len+1; if(l2>*cap){*cap=l2*2;*out=realloc(*out,*cap);} (*out)[(*len)++]=','; } first=0; emit(e,out,cap,len); e=e->next; } - l=*len+1; if(l>*cap){*cap=l*2;*out=realloc(*out,*cap);} (*out)[(*len)++]=']'; return; - } - case JL_OBJECT: { - size_t l = (*len)+1; if(l>*cap){*cap=l*2;*out=realloc(*out,*cap);} (*out)[(*len)++]='{'; - jl_val_t *p=v->child; int first=1; - while(p){ if(p->str){ if(!first){ size_t l2=*len+1; if(l2>*cap){*cap=l2*2;*out=realloc(*out,*cap);} (*out)[(*len)++]=','; } first=0; + case JL_ARRAY:{ + size_t l=*len+1; if(l>*cap){*cap=l*2;*out=realloc(*out,*cap);} (*out)[(*len)++]='['; + jl_val_t*e=v->child;int first=1; + while(e){ if(!first){ size_t l2=*len+1; if(l2>*cap){*cap=l2*2;*out=realloc(*out,*cap);} (*out)[(*len)++]=','; } + first=0; emit(e,out,cap,len); e=e->next; } + l=*len+1; if(l>*cap){*cap=l*2;*out=realloc(*out,*cap);} (*out)[(*len)++]=']'; return; } + case JL_OBJECT:{ + size_t l=*len+1; if(l>*cap){*cap=l*2;*out=realloc(*out,*cap);} (*out)[(*len)++]='{'; + jl_val_t*p=v->child;int first=1; + while(p){ if(p->str){ + if(!first){ size_t l2=*len+1; if(l2>*cap){*cap=l2*2;*out=realloc(*out,*cap);} (*out)[(*len)++]=','; } + first=0; escstr(p->str,out,cap,len); - size_t l2=*len+1; if(l2>*cap){*cap=l2*2;*out=realloc(*out,*cap);} (*out)[(*len)++]=':'; - if(p->type==JL_OBJECT && p->child){ emit(p->child,out,cap,len); } - else if(p->type==JL_STRING){ escstr(p->str,out,cap,len); } - else { snprintf(buf,sizeof buf,"%g",p->num); size_t lb=strlen(buf); if(*len+lb+1>*cap){*cap=(*len+lb+2)*2;*out=realloc(*out,*cap);} memcpy(*out+*len,buf,lb); *len+=lb; } + size_t l3=*len+1; if(l3>*cap){*cap=l3*2;*out=realloc(*out,*cap);} (*out)[(*len)++]=':'; + /* value is the pair's child OR the pair holds value via child */ + if(p->child) emit(p->child,out,cap,len); else { *out[*len]='n'; /* shouldn't happen */ } + /* for non-pair objects where a STRING key sits directly: handled above via child */ } p=p->next; } - l=*len+1; if(l>*cap){*cap=l*2;*out=realloc(*out,*cap);} (*out)[(*len)++]='}'; return; - } + l=*len+1; if(l>*cap){*cap=l*2;*out=realloc(*out,*cap);} (*out)[(*len)++]='}'; return; } } - if (buf[0]) { size_t lb=strlen(buf); if(*len+lb+1>*cap){*cap=(*len+lb+2)*2;*out=realloc(*out,*cap);} memcpy(*out+*len,buf,lb); *len+=lb; } + if(buf[0]){ size_t lb=strlen(buf); + if(*len+lb+1>*cap){*cap=(*len+lb+2)*2;*out=realloc(*out,*cap);} + memcpy(*out+*len,buf,lb);*len+=lb; } } -char *jl_stringify(const jl_val_t *v) { - (void)v; char *out=NULL; size_t cap=256,len=0; - /* stringify mutable copy path: emit takes jl_val_t* ; cast away const (safe: we don't mutate) */ - emit((jl_val_t*)v, &out, &cap, &len); +char *jl_stringify(const jl_val_t *v){ + char *out=NULL; size_t cap=256,len=0; + emit((jl_val_t*)v,&out,&cap,&len); if(!out){ out=(char*)malloc(1); out[0]=0; } else { char *t=(char*)realloc(out,len+1); t[len]=0; out=t; } return out; } -/* ---- builders ---- */ -jl_val_t *jl_new_string(const char *s){ jl_val_t *v=newval(JL_STRING); v->str=strdup(s); v->strlen=strlen(s); return v; } +jl_val_t *jl_new_string(const char *s){ jl_val_t *v=newval(JL_STRING); v->str=strdup(s?s:""); v->strlen=strlen(v->str); return v; } jl_val_t *jl_new_number(double n){ jl_val_t *v=newval(JL_NUMBER); v->num=n; return v; } -jl_val_t *jl_new_bool(int b){ jl_val_t *v=newval(JL_BOOL); v->num=b; return v; } +jl_val_t *jl_new_bool(int b){ jl_val_t *v=newval(JL_BOOL); v->num=b?1:0; return v; } jl_val_t *jl_new_object(void){ return newval(JL_OBJECT); } jl_val_t *jl_new_array(void){ return newval(JL_ARRAY); } -void jl_obj_set(jl_val_t *obj, const char *key, jl_val_t *val){ +void jl_obj_set(jl_val_t *obj,const char *key,jl_val_t *val){ jl_val_t *pair=newval(JL_OBJECT); pair->str=strdup(key); pair->strlen=strlen(key); pair->child=val; if(!obj->child){obj->child=pair;}else{ jl_val_t*t=obj->child; while(t->next)t=t->next; t->next=pair; } } -void jl_arr_push(jl_val_t *arr, jl_val_t *val){ - jl_val_t *e=newval(JL_ARRAY); /* reuse array node as element holder? simpler: push val directly chained */ - /* To keep iteration simple, store elements as siblings under child (type kept as the val's type) */ - if(!arr->child){arr->child=val; val->next=NULL;} - else { jl_val_t *t=arr->child; while(t->next)t=t->next; t->next=val; } - arr->count++; +void jl_arr_push(jl_val_t *arr,jl_val_t *val){ + if(!arr->child){arr->child=val;val->next=NULL;} + else{ jl_val_t*t=arr->child; while(t->next)t=t->next; t->next=val; } } -void jl_set_type_string(jl_val_t *v, const char *s){ if(v->type==JL_STRING){free(v->str);v->str=strdup(s);v->strlen=strlen(s);} } -void jl_set_type_number(jl_val_t *v, double n){ v->num=n; } diff --git a/PS4RP/jsonlite.h b/PS4RP/jsonlite.h index c2e3e13..94439d5 100644 --- a/PS4RP/jsonlite.h +++ b/PS4RP/jsonlite.h @@ -1,52 +1,29 @@ -/* jsonlite.h - tiny JSON parser/serializer for PS4RP. - * Self-contained: parse gateway frames + config + API responses. - * Public domain, no dependencies beyond <stdlib.h>/<string.h>. */ +/* jsonlite.h - tiny JSON parser/serializer (self contained). */ #ifndef JSONLITE_H #define JSONLITE_H - #include <stddef.h> #include <stdint.h> - typedef enum { JL_NULL, JL_BOOL, JL_NUMBER, JL_STRING, JL_OBJECT, JL_ARRAY } jl_type_t; - typedef struct jl_val { jl_type_t type; - /* string / object key */ - char *str; /* for JL_STRING: own data. for OBJECT entries: key */ + char *str; /* string text OR object-entry key */ size_t strlen; - /* number */ double num; - /* bool (1/0) reuses num */ - /* object / array contents */ - struct jl_val *child; /* first pair (object) or first element (array) */ - struct jl_val *next; /* next sibling: for object pairs and array elems */ - int32_t count; /* pair/element count (optional for iteration) */ + struct jl_val *child; /* object: first pair ; array: first elem */ + struct jl_val *next; /* object pair chain OR array elem chain */ + int32_t count; } jl_val_t; - typedef struct { const char *cur; const char *end; int err; } jl_parse_t; - -/* Parse a NUL-terminated (or len-bounded) JSON string. Returns root or NULL on error. */ jl_val_t *jl_parse(const char *s, size_t len); - -/* Free a value tree. */ void jl_free(jl_val_t *v); - -/* Object/array helpers (returns NULL if missing). */ const jl_val_t *jl_obj_get(const jl_val_t *obj, const char *key); const jl_val_t *jl_arr_at(const jl_val_t *arr, size_t i); - -/* Serialize a value to a malloc'd compact JSON string (caller frees). */ char *jl_stringify(const jl_val_t *v); - -/* Build helpers. */ jl_val_t *jl_new_string(const char *s); jl_val_t *jl_new_number(double n); jl_val_t *jl_new_bool(int b); jl_val_t *jl_new_object(void); jl_val_t *jl_new_array(void); -void jl_obj_set(jl_val_t *obj, const char *key, jl_val_t *val); /* moves val */ -void jl_arr_push(jl_val_t *arr, jl_val_t *val); /* moves val */ -void jl_set_type_string(jl_val_t *v, const char *s); -void jl_set_type_number(jl_val_t *v, double n); - -#endif +void jl_obj_set(jl_val_t *obj, const char *key, jl_val_t *val); +void jl_arr_push(jl_val_t *arr, jl_val_t *val); +#endif \ No newline at end of file diff --git a/PS4RP/log.c b/PS4RP/log.c new file mode 100644 index 0000000..0ac3c4b --- /dev/null +++ b/PS4RP/log.c @@ -0,0 +1,33 @@ +/* log.c */ +#include "log.h" +#include <stdarg.h> +#include <time.h> +#include <string.h> + +static FILE *g_log = NULL; + +void log_init(const char *path) { + if (g_log) return; + g_log = fopen(path, "ab"); + if (g_log) setvbuf(g_log, NULL, _IONBF, 0); +} + +void log_msg(const char *fmt, ...) { + if (!g_log) { g_log = stderr; } + va_list ap; + va_start(ap, fmt); + time_t t = time(NULL); + struct tm *tm = localtime(&t); + char ts[20]; + strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M", tm); + fprintf(g_log, "[%s] ", ts); + vfprintf(g_log, fmt, ap); + fprintf(g_log, "\n"); + va_end(ap); + fflush(g_log); +} + +void log_close(void) { + if (g_log && g_log != stderr) fclose(g_log); + g_log = NULL; +} diff --git a/PS4RP/log.h b/PS4RP/log.h new file mode 100644 index 0000000..5f76701 --- /dev/null +++ b/PS4RP/log.h @@ -0,0 +1,8 @@ +/* log.h - simple file logger (falls back to stderr). */ +#ifndef LOG_H +#define LOG_H +#include <stdio.h> +void log_init(const char *path); +void log_msg(const char *fmt, ...); +void log_close(void); +#endif \ No newline at end of file diff --git a/PS4RP/main.c b/PS4RP/main.c new file mode 100644 index 0000000..61c4f64 --- /dev/null +++ b/PS4RP/main.c @@ -0,0 +1,99 @@ +/* main.c - PS4RP daemon: detect game -> Discord presence. Runs as GoldHEN payload. */ +#include "cfg.h" +#include "log.h" +#include "http.h" +#include "ws.h" +#include "discord.h" +#include "detect.h" +#include "jsonlite.h" +#include "b64.h" +#include <orbis/UserService.h> +#include <orbis/libkernel.h> +#include <unistd.h> +#include <stdint.h> +#include <string.h> +#include <time.h> + +extern cfg_t g_cfg; + +static int ensure_token(void){ + int64_t now=time(NULL); + if(g_cfg.access_token[0] && now < g_cfg.token_expires_at - 120) return 0; /* fresh */ + char at[256]="", rt[256]=""; int64_t exp=0; + int rc = http_oauth_token(g_cfg.client_id, g_cfg.client_secret, + g_cfg.auth_code, + g_cfg.refresh_token[0]?g_cfg.refresh_token:NULL, + at, sizeof at, g_cfg.refresh_token[0]?rt:NULL, g_cfg.refresh_token[0]?sizeof g_cfg.refresh_token:0, + &exp); + if(rc==0 && at[0]){ + strncpy(g_cfg.access_token, at, sizeof g_cfg.access_token-1); + if(rt[0]) strncpy(g_cfg.refresh_token, rt, sizeof g_cfg.refresh_token-1); + g_cfg.token_expires_at = exp; + memset(g_cfg.auth_code,0,sizeof g_cfg.auth_code); /* one-time use */ + cfg_save(CFG_PATH, &g_cfg); + log_msg("token refreshed, expires_in=%lds", (long)(exp-now)); + return 0; + } + log_msg("oauth failed rc=%d", rc); + return rc; +} + +static void set_game_presence(discord_t *d, const char *name){ + if(!name||!*name){ discord_clear_presence(d); return; } + discord_set_presence(d, + "On PS4", /* state */ + name, /* details = game name */ + 1, 1, /* party */ + "ps4rp:playing", /* large_image key (asset) */ + name); /* large_text */ + log_msg("presence: %s", name); +} + +int main(int argc, char **argv){ + (void)argc;(void)argv; + /* GoldHEN payloads: write to /data/PS4RP */ + cfg_load(CFG_PATH, &g_cfg); + if(!g_cfg.enabled){ log_msg("disabled in config; exiting"); return 0; } + if(!g_cfg.client_id[0] || strcmp(g_cfg.client_id,"SET_ME")==0){ + log_msg("FATAL: set client_id + auth_code (or refresh) in %s", CFG_PATH); + return 1; + } + discord_t dc; + /* token first */ + if(ensure_token()!=0){ + log_msg("no valid token; cannot connect gateway"); + return 2; + } + if(discord_connect(&dc, g_cfg.access_token, NULL)!=0){ + log_msg("gateway connect failed"); + return 3; + } + char last_name[128]=""; + int64_t last_change = 0; + for(;;){ + char name[128]=""; char path[128]=""; + int active = detect_foreground_active(); + if(active && detect_current_game(name,sizeof name,path,sizeof path)==0 && name[0]){ + int64_t now=time(NULL); + if(strcmp(name,last_name)!=0 || now-last_change > 300){ + set_game_presence(&dc, name); + strncpy(last_name, name, sizeof last_name-1); + last_change = now; + } + } else { + if(last_name[0] || !active){ + set_game_presence(&dc, NULL); /* clear when nothing foreground */ + last_name[0]=0; last_change=time(NULL); + } + } + if(discord_tick(&dc) != 0){ + log_msg("gateway dropped; reconnecting..."); + discord_reconnect(&dc); + } + usleep((useconds_t)(g_cfg.poll_interval_s * 1000000)); + } + discord_clear_presence(&dc); + ws_close(&dc.ws); + log_close(); + return 0; +} diff --git a/PS4RP/ws.c b/PS4RP/ws.c new file mode 100644 index 0000000..1a75d24 --- /dev/null +++ b/PS4RP/ws.c @@ -0,0 +1,172 @@ +/* ws.c - WebSocket client over SceNet + PS4 LibreSSL (OpenSSL-style) TLS. + * The SDK's libSceLibreSSL.so exports SSL_CTX_new / SSL_new / SSL_connect / + * SSL_write / SSL_read / SSL_shutdown / SSL_set_fd / SSL_get_error (OpenSSL ABI), + * which is what we use here for the TLS transport. */ +#include "ws.h" +#include "b64.h" +#include "log.h" +#include <orbis/Net.h> +#include <orbis/Sysmodule.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <sys/socket.h> +#include <string.h> +#include <stdlib.h> +#include <unistd.h> + +/* OpenSSL-style SSL objects (opaque). We only use pointers + the exported API. */ +typedef struct ssl_ctx_st SSL_CTX; +typedef struct ssl_st SSL; + +/* exported from libSceLibreSSL.so (OpenSSL ABI) */ +extern SSL_CTX *SSL_CTX_new(const void *method); +extern void SSL_CTX_free(SSL_CTX *ctx); +extern void SSL_CTX_set_verify(SSL_CTX *ctx, int mode, void *cb); +extern SSL *SSL_new(SSL_CTX *ctx); +extern void SSL_free(SSL *s); +extern int SSL_set_fd(SSL *s, int fd); +extern int SSL_set_connect_state(SSL *s); +extern int SSL_connect(SSL *s); +extern int SSL_write(SSL *s, const void *buf, int num); +extern int SSL_read(SSL *s, void *buf, int num); +extern int SSL_shutdown(SSL *s); +extern int SSL_get_error(SSL *s, int ret); +extern const void *SSLv23_client_method(void); +extern int SSL_set_tls_host(SSL *s, const char *hostname); /* if exported */ + +/* net pool id (keep once) */ +static int s_net_ready = 0; +static int s_net_mem = 0; + +static int net_ensure(void){ + if(s_net_ready) return 0; + uint32_t ur = sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_NET); + if((int)ur < 0){ log_msg("load NET fail %d", (int)ur); return -1; } + /* sceSslInit is NOT needed for LibreSSL path; SceNet init only */ + if(sceNetInit() < 0){ log_msg("sceNetInit fail"); return -2; } + s_net_mem = (int)sceNetPoolCreate("ps4rpNet", 128*1024, 0); + if(s_net_mem < 0){ log_msg("net pool fail %d", s_net_mem); return -3; } + s_net_ready = 1; + return 0; +} + +int ws_connect(ws_t *w, const char *host, int port, const char *resource, const char *key){ + memset(w,0,sizeof(*w)); + if(net_ensure()<0) return -1; + /* resolve host */ + int32_t rid = sceNetResolverCreate("ps4rpR", 0, 0); + OrbisNetInAddr in; memset(&in,0,sizeof in); + int resolved = 0; + if(rid >= 0){ + int32_t rr = sceNetResolverStartNtoa(rid, host, &in, 5, 3, 0); + sceNetResolverDestroy(rid); + resolved = (rr >= 0); + } + if(!resolved){ + struct in_addr ia = { .s_addr = inet_addr(host) }; + if(ia.s_addr == 0xffffffff){ log_msg("resolve fail: %s", host); return -4; } + in.s_addr = ia.s_addr; + } + /* TCP socket */ + int32_t fd = sceNetSocket("ps4rpWs", ORBIS_NET_AF_INET, ORBIS_NET_SOCK_STREAM, 0); + if(fd < 0){ log_msg("socket fail %d",fd); return -5; } + OrbisNetSockaddr sa; memset(&sa,0,sizeof sa); + sa.len = (uint8_t)sizeof(sa); + sa.sa_family = (OrbisNetSaFamily_t)ORBIS_NET_AF_INET; + sa.sa_data[0] = (char)((port >> 8) & 0xff); + sa.sa_data[1] = (char)(port & 0xff); + sa.sa_data[2] = (char)(in.s_addr & 0xff); + sa.sa_data[3] = (char)((in.s_addr >> 8) & 0xff); + sa.sa_data[4] = (char)((in.s_addr >> 16) & 0xff); + sa.sa_data[5] = (char)((in.s_addr >> 24) & 0xff); + if(sceNetConnect(fd, &sa, sizeof sa) < 0){ + log_msg("connect fail to %s:%d", host, port); + sceNetSocketClose(fd); return -6; + } + w->fd = fd; w->sock = fd; w->connected = 1; + /* TLS via LibreSSL */ + SSL_CTX *ctx = SSL_CTX_new(SSLv23_client_method()); + if(!ctx){ log_msg("SSL_CTX_new fail"); goto fail; } + SSL *ssl = SSL_new(ctx); + if(!ssl){ log_msg("SSL_new fail"); SSL_CTX_free(ctx); goto fail; } + if(SSL_set_fd(ssl, (int)fd) != 1){ log_msg("SSL_set_fd fail"); SSL_free(ssl); SSL_CTX_free(ctx); goto fail; } + SSL_set_connect_state(ssl); + if(SSL_connect(ssl) != 1){ log_msg("SSL_connect fail"); SSL_free(ssl); SSL_CTX_free(ctx); goto fail; } + w->ssl_ctx = (int32_t)(intptr_t)ctx; /* stash for teardown */ + w->ssl = (int32_t)(intptr_t)ssl; + /* HTTP Upgrade handshake */ + char req[512]; int n=snprintf(req,sizeof req, + "GET %s HTTP/1.1\r\nHost: %s:%d\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n" + "Sec-WebSocket-Key: %s\r\nSec-WebSocket-Version: 13\r\nOrigin: https://discord.com\r\n\r\n", + resource?resource:"/", host, port, key); + if(SSL_write(ssl, req, n) <= 0){ log_msg("SSL_write hs fail"); goto fail; } + char hdr[512]; int hlen=0, rd; + while(hlen<(int)sizeof hdr-4){ + rd = SSL_read(ssl, hdr+hlen, 1); + if(rd<=0) break; + hlen++; hdr[hlen]=0; + if(hlen>=4 && memcmp(hdr+hlen-4,"\r\n\r\n",4)==0) break; + } + if(hlen<12 || !strstr(hdr,"101")){ log_msg("no 101: %.40s", hdr); goto fail; } + log_msg("ws: connected (handshake ok)"); + return 0; +fail: + if(w->ssl) SSL_free((SSL*)(intptr_t)w->ssl); + if(w->ssl_ctx) SSL_CTX_free((SSL_CTX*)(intptr_t)w->ssl_ctx); + sceNetSocketClose(w->fd); + w->connected=0; w->ssl=0; w->ssl_ctx=0; w->fd=0; + return -9; +} + +int ws_send_text(ws_t *w, const char *msg, size_t len){ + if(!w->connected) return -1; + unsigned char frame[2048]; size_t f=0; + frame[f++]=0x81; + if(len<126){ frame[f++]=(unsigned char)(0x80|len); } + else if(len<65536){ frame[f++]=0x80|126; frame[f++]=(len>>8)&0xff; frame[f++]=len&0xff; } + else { frame[f++]=0x80|127; for(int i=7;i>=0;i--)frame[f++]=(len>>(i*8))&0xff; } + unsigned char mk[4]={0x12,0x34,0x56,0x78}; + memcpy(frame+f, mk, 4); f+=4; + for(size_t i=0;i<len;i++) frame[f+i]=(msg[i]^mk[i%4]); + f+=len; + return SSL_write((SSL*)(intptr_t)w->ssl, frame, (int)f); +} + +int ws_send_ping(ws_t *w){ + if(!w->connected) return -1; + unsigned char p[2]={0x89,0x00}; + return SSL_write((SSL*)(intptr_t)w->ssl, p, 2); +} + +int ws_recv_frame(ws_t *w, char *buf, size_t cap, int *opcode_out, int *fin_out){ + if(!w->connected) return -1; + SSL *s=(SSL*)(intptr_t)w->ssl; + unsigned char h[2]; + if(SSL_read(s, h, 2)!=2) return -1; + int fin=(h[0]&0x80)!=0, op=h[0]&0x0f, masked=(h[1]&0x80)!=0; + size_t plen=h[1]&0x7f; + if(opcode_out)*opcode_out=op; if(fin_out)*fin_out=fin; + if(plen==126){ unsigned char e[2]; if(SSL_read(s,e,2)!=2)return -1; plen=(e[0]<<8)|e[1]; } + else if(plen==127){ unsigned char e[8]; if(SSL_read(s,e,8)!=8)return -1; plen=0; for(int i=4;i<8;i++)plen=(plen<<8)|e[i]; } + if(masked){ unsigned char mk[4]; if(SSL_read(s,mk,4)!=4)return -1; } + size_t i=0; + while(i<plen){ + size_t want=plen-i; if(want>cap-1)want=cap-1; + int rd=SSL_read(s, buf+i, (int)want); + if(rd<=0) return -1; i+=rd; + } + buf[i>cap-1?cap-1:i]=0; + return (int)i; +} + +int ws_close(ws_t *w){ + if(!w->connected) return 0; + unsigned char c[2]={0x88,0x00}; + SSL_write((SSL*)(intptr_t)w->ssl, c, 2); + SSL_shutdown((SSL*)(intptr_t)w->ssl); + SSL_free((SSL*)(intptr_t)w->ssl); + SSL_CTX_free((SSL_CTX*)(intptr_t)w->ssl_ctx); + sceNetSocketClose(w->fd); + w->connected=0; w->sock=0; w->ssl=0; w->ssl_ctx=0; w->fd=0; + return 0; +} \ No newline at end of file diff --git a/PS4RP/ws.h b/PS4RP/ws.h new file mode 100644 index 0000000..d9adb18 --- /dev/null +++ b/PS4RP/ws.h @@ -0,0 +1,13 @@ +/* ws.h - minimal Discord gateway WebSocket (text frames + heartbeat). */ +#ifndef WS_H +#define WS_H +#include <stdint.h> +#include <stddef.h> +/* opaque socket handle */ +typedef struct { int32_t sock; int32_t ssl; int32_t ssl_ctx; int32_t connected; int32_t fd; } ws_t; +int ws_connect(ws_t *w, const char *host, int port, const char *resource, const char *key); +int ws_send_text(ws_t *w, const char *msg, size_t len); +int ws_send_ping(ws_t *w); +int ws_recv_frame(ws_t *w, char *buf, size_t cap, int *opcode_out, int *fin_out); +int ws_close(ws_t *w); +#endif diff --git a/config/config.json b/config/config.json new file mode 100644 index 0000000..f085d2e --- /dev/null +++ b/config/config.json @@ -0,0 +1,11 @@ +{ + "client_id": "SET_ME", + "client_secret": "", + "access_token": "", + "refresh_token": "", + "auth_code": "", + "token_expires_at": 0, + "enabled": 1, + "poll_interval_s": 12, + "presence_state": "On PS4" +} diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..b52d912 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# build.sh - PS4RP: compile payload ELF + fself for PS4. +# Usage: ./scripts/build.sh [elf|fself|all] default: all +set -euo pipefail +SDK="${OO_PS4_TOOLCHAIN:-/Users/mac/PS4Toolchain/OpenOrbis/PS4Toolchain}" +LLVM_BIN="${LLVM_HOME:-/usr/local/opt/llvm}/bin" +export PATH="$LLVM_BIN:$SDK/bin/macos:$PATH" +CC="$LLVM_BIN/clang" +LD="${LLD:-/usr/local/opt/lld@21/bin/ld.lld}" +TARGET="x86_64-pc-freebsd12-elf" +CFLAGS="--target=$TARGET -fPIC -std=gnu11 -Wall -Wno-unused \ + -Wno-int-conversion -Wno-incompatible-pointer-types \ + -isystem $SDK/include" +LIBS="-lc -lkernel -lSceNet -lSceNetCtl -lSceSsl -lSceHttp -lSceSysmodule \ + -lSceUserService -lSceAppInstUtil -lSceAppContent" +LDFLAGS="-m elf_x86_64 -pie --eh-frame-hdr -L$SDK/lib $LIBS $SDK/lib/crt1.o --script $SDK/link.x" +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" +OUT="$ROOT/build"; mkdir -p "$OUT" +echo "=== compiling ===" +for f in log cfg jsonlite b64 http ws detect discord main; do + "$CC" $CFLAGS -c -o "$OUT/$f.o" "PS4RP/$f.c" || { echo "compile $f FAILED"; exit 1; } +done +echo "=== linking ($LD) ===" +"$LD" $OUT/*.o -o "$OUT/ps4rp.elf" $LDFLAGS || { echo "link FAILED"; exit 1; } +echo "ELF -> $OUT/ps4rp.elf" +MODE="${1:-all}" +if [ "$MODE" = "fself" ] || [ "$MODE" = "all" ]; then + "$SDK/bin/macos/create-fself-macos" -in="$OUT/ps4rp.elf" \ + -out="$OUT/ps4rp.fself" --eboot "$OUT/ps4rp.bin" --paid 0x3800000000000011 + echo "FSELF -> $OUT/ps4rp.fself" +fi +echo "done." \ No newline at end of file