From d40fc600c48af7a7838bc2a42385bcb288bf9b8e Mon Sep 17 00:00:00 2001 From: SirHumza <204067870+SirHumza@users.noreply.github.com> Date: Fri, 14 Aug 2026 20:24:24 +0200 Subject: [PATCH] revise: fix TLS handshake, heartbeat ack, RESUME, seq parse, party ws: SSL_connect on the non-blocking socket now polls WANT_READ/WANT_WRITE with a deadline (previously it would fail the very first handshake); handshake response read in chunks so the 5s deadline can't spuriously trip; ws_send_text errors on oversized frames instead of silently truncating; added ws_send_pong for RFC6455 server pings. discord: heartbeat ack tracking -> reconnect on 2x-interval silence; handle op 7 (reconnect) and op 9 (invalid session, clears session_id); parse the sequence from the correct "s" field (was "seq", never matched, heartbeat always sent d:0); READY captures session_id for RESUME; reconnect now RESUMEs (op 6) when a session exists and falls back to IDENTIFY on invalid-session; party size now emitted in the activity; dropped the unused 'intent' arg; zeroed gateway-fetch buffer. main: use config presence_state (was hardcoded); reset last_name after reconnect so the current game is re-pushed immediately. cfg: clamp poll_interval_s to [5,300]; atomic config save (tmp+rename). log: seconds in timestamps; rotate log at 512 KB. jsonlite: jl_obj_get/jl_arr_at return non-const (fixes const-drop warnings); stringify emits proper "null" for missing pair values. detect: trim whitespace around app.xml title; drop per-poll log spam. --- orbisrpc/cfg.c | 19 ++++- orbisrpc/detect.c | 3 +- orbisrpc/discord.c | 190 +++++++++++++++++++++++++++++++------------- orbisrpc/discord.h | 13 +-- orbisrpc/jsonlite.c | 17 ++-- orbisrpc/jsonlite.h | 6 +- orbisrpc/log.c | 11 ++- orbisrpc/main.c | 23 +++--- orbisrpc/ws.c | 55 ++++++++----- orbisrpc/ws.h | 1 + 10 files changed, 231 insertions(+), 107 deletions(-) diff --git a/orbisrpc/cfg.c b/orbisrpc/cfg.c index 4854f69..5eee333 100644 --- a/orbisrpc/cfg.c +++ b/orbisrpc/cfg.c @@ -11,9 +11,15 @@ cfg_t g_cfg; void cfg_defaults(cfg_t *c) { memset(c, 0, sizeof(*c)); c->enabled = 1; - c->poll_interval_s = 15; + c->poll_interval_s = 12; strncpy(c->client_id, "SET_ME", sizeof(c->client_id)-1); - strncpy(c->presence_state, "Playing", sizeof(c->presence_state)-1); + strncpy(c->presence_state, "On PS4", sizeof(c->presence_state)-1); +} + +/* keep the daemon sane if the user puts junk in config */ +static void clamp_cfg(cfg_t *c){ + if(c->poll_interval_s < 5) c->poll_interval_s = 5; + if(c->poll_interval_s > 300) c->poll_interval_s = 300; } int cfg_load(const char *path, cfg_t *c) { @@ -42,6 +48,7 @@ int cfg_load(const char *path, cfg_t *c) { 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); + clamp_cfg(c); return 0; } @@ -57,7 +64,11 @@ void cfg_save(const char *path, const cfg_t *c) { 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); } + /* write tmp + rename so a power loss can't corrupt the config */ + char tmp[160]; + snprintf(tmp, sizeof tmp, "%s.tmp", path); + FILE *f = fopen(tmp, "wb"); + if (f) { fputs(s, f); fclose(f); rename(tmp, path); } + else { log_msg("cfg_save: cannot write %s", tmp); } free(s); jl_free(r); } \ No newline at end of file diff --git a/orbisrpc/detect.c b/orbisrpc/detect.c index ed32c43..e4e0624 100644 --- a/orbisrpc/detect.c +++ b/orbisrpc/detect.c @@ -107,9 +107,11 @@ static int appxml_title(const char *titleId, char *out, size_t cap){ p = strchr(p, '>'); if(!p) return -1; p++; + while(*p==' '||*p=='\t'||*p=='\r'||*p=='\n') p++; /* trim leading ws */ size_t i=0; while(*p && *p!='<' && *p!='\n' && *p!='\r' && i0 && (out[i-1]==' '||out[i-1]=='\t')) out[--i]=0; /* trim trailing */ return (i>0)?0:-1; } @@ -152,6 +154,5 @@ int detect_current_game(char *out_name, size_t cap, char *out_path, size_t p_cap strncpy(out_name, "(unknown game)", cap-1); out_name[cap-1]=0; } if(out_path){ snprintf(out_path, p_cap, "/data/orbisRPC/.lastgame/%s", titleId[0]?titleId:"unknown"); } - log_msg("detect: active name=%s tid=%s", out_name, titleId); return 0; } \ No newline at end of file diff --git a/orbisrpc/discord.c b/orbisrpc/discord.c index ddeeb6b..92cf3bb 100644 --- a/orbisrpc/discord.c +++ b/orbisrpc/discord.c @@ -1,4 +1,7 @@ -/* discord.c - Discord gateway client. */ +/* discord.c - Discord gateway client. + * User OAuth2 connection: IDENTIFY carries required connection properties, + * no bot intents, presence via op 3. Handles heartbeat acks, RESUME on + * reconnect, invalid-session, server pings and close frames. */ #include "discord.h" #include "http.h" #include "b64.h" @@ -11,6 +14,8 @@ #include #include +#define GATEWAY_DEFAULT "wss://gateway.discord.gg/?v=10&encoding=json" + static void make_key(char *out){ unsigned char b[16]; int fd=open("/dev/urandom",O_RDONLY); @@ -19,47 +24,49 @@ static void make_key(char *out){ 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 (public endpoint, no bot auth needed) */ - char resp[512]; +/* Parse "wss://host[:port][/path?query]" into its parts. */ +static void parse_gateway_url(const char *url, char *host, size_t hcap, + int *port, char *res, size_t rcap){ + const char *p=strstr(url,"://"); + p = p ? p+3 : url; + size_t hl=0; + while(*p && *p!=':' && *p!='/' && *p!='?' && hl='0'&&*p<='9'){ *port=*port*10+(*p-'0'); p++; } + } + if(!*port) *port=443; + if(*p=='/'){ + size_t rl=0; + while(*p && rltype==JL_STRING){ strncpy(url,u->str,sizeof url-1); url[sizeof url-1]=0; } - jl_free(r); + if(!r) return; + const jl_val_t *u=jl_obj_get(r,"url"); + if(u && u->type==JL_STRING){ + strncpy(url,u->str,cap-1); url[cap-1]=0; } - char key[64]=""; make_key(key); - const char *res="/?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. User OAuth2 connection: no intents (bot-only field), but the - * gateway REQUIRES connection properties os/browser/device. */ + jl_free(r); +} + +static void send_identify(discord_t *d, const char *token){ + /* User OAuth2 connection: no intents (bot-only field), but the gateway + * REQUIRES connection properties os/browser/device. */ jl_val_t *root=jl_new_object(); jl_obj_set(root,"op",jl_new_number(2)); jl_val_t *dd=jl_new_object(); @@ -72,6 +79,47 @@ int discord_connect(discord_t *d, const char *token, const char *intent){ jl_obj_set(root,"d",dd); char *s=jl_stringify(root); jl_free(root); ws_send_text(&d->ws,s,strlen(s)); free(s); +} + +static void send_resume(discord_t *d){ + jl_val_t *root=jl_new_object(); + jl_obj_set(root,"op",jl_new_number(6)); + jl_val_t *dd=jl_new_object(); + jl_obj_set(dd,"token",jl_new_string(d->token)); + jl_obj_set(dd,"session_id",jl_new_string(d->session_id)); + jl_obj_set(dd,"seq",jl_new_number((double)d->seq)); + jl_obj_set(root,"d",dd); + char *s=jl_stringify(root); jl_free(root); + ws_send_text(&d->ws,s,strlen(s)); free(s); +} + +/* resume=1: reuse the stored session (fast reconnect). Otherwise IDENTIFY. */ +int discord_connect(discord_t *d, const char *token, int resume){ + memset(d,0,sizeof(*d)); + strncpy(d->token, token, sizeof d->token-1); + char url[512]; fetch_gateway_url(url,sizeof url); + char host[128]; int port=443; char res[128]; + parse_gateway_url(url, host, sizeof host, &port, res, sizeof res); + char key[64]=""; make_key(key); + 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); d->last_ack=d->last_heartbeat; + /* 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; + } + jl_free(h); + } + if(!d->hb_interval_ms) d->hb_interval_ms=45000; + if(resume && d->session_id[0]){ send_resume(d); log_msg("discord: resume sent"); } + else { send_identify(d, token); } log_msg("discord: gateway connected, hb=%lds", (long)(d->hb_interval_ms/1000)); return 0; } @@ -79,16 +127,24 @@ int discord_connect(discord_t *d, const char *token, const char *intent){ 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); + if(party_size>0 && party_max>0){ + jl_val_t *pty=jl_new_object(); + jl_obj_set(pty,"size",jl_new_array()); + jl_arr_push(jl_obj_get(pty,"size"), jl_new_number(party_size)); + jl_arr_push(jl_obj_get(pty,"size"), jl_new_number(party_max)); + jl_obj_set(act,"party",pty); + } + if(large_image || large_text){ + 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",jl_new_string("online")); @@ -117,6 +173,11 @@ int discord_clear_presence(discord_t *d){ int discord_tick(discord_t *d){ if(!d->connected) return -1; int64_t now=time(NULL); + /* heartbeat: if the gateway hasn't acked in 2 intervals, it's gone */ + if(now - d->last_ack > (d->hb_interval_ms/1000)*2 + 5){ + log_msg("heartbeat timeout (no ack)"); + return -1; + } 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)); @@ -127,23 +188,44 @@ int discord_tick(discord_t *d){ int nr=ws_recv_frame(&d->ws,buf,sizeof buf,&op,&fin); if(nr<0){ return -1; } if(nr==0){ return 0; } /* no complete frame yet — not an error */ - 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); + switch(op){ + case 0: /* DISPATCH: "s" is the sequence, "t" the event name */ + { + const char *p=strstr(buf,"\"s\":"); + if(p){ p+=4; d->seq=(int)strtol(p,NULL,10); } + const char *t=strstr(buf,"\"t\":"); + if(t && strncmp(t+4,"\"READY\"",7)==0){ + jl_val_t *r=jl_parse(buf,nr); + if(r){ + const jl_val_t *dd=jl_obj_get(r,"d"); + const jl_val_t *si=dd?jl_obj_get(dd,"session_id"):NULL; + if(si&&si->type==JL_STRING) + strncpy(d->session_id,si->str,sizeof d->session_id-1); + jl_free(r); + } } } + break; + case 7: /* RECONNECT: server wants us to reconnect */ + log_msg("gateway: reconnect requested"); + return -1; + case 9: /* INVALID_SESSION: resume rejected; force a fresh IDENTIFY */ + log_msg("gateway: invalid session"); + d->session_id[0]=0; + return -1; + case 11: /* HEARTBEAT_ACK */ + d->last_ack=now; + break; + default: + break; } return 0; } +/* Reconnect: try RESUME with the stored session; falls back to IDENTIFY + * automatically (op 9 clears the session and triggers a fresh connect). */ int discord_reconnect(discord_t *d){ ws_close(&d->ws); d->connected=0; - return discord_connect(d, d->token, NULL); -} + int rc=discord_connect(d, d->token, 1); + return rc; +} \ No newline at end of file diff --git a/orbisrpc/discord.h b/orbisrpc/discord.h index 01d89d7..db888ea 100644 --- a/orbisrpc/discord.h +++ b/orbisrpc/discord.h @@ -4,19 +4,20 @@ #include "ws.h" typedef struct { ws_t ws; - int32_t seq; + int32_t seq; /* last dispatch sequence (for RESUME) */ char session_id[64]; char token[256]; - int64_t last_heartbeat; + int64_t last_heartbeat; /* when we last sent op 1 */ + int64_t last_ack; /* when the gateway last acked (op 11) */ 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" */ +/* resume=1 reuses the stored session_id/seq (RESUME op 6), else IDENTIFY. */ +int discord_connect(discord_t *d, const char *token, int resume); 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 */ + const char *large_text); /* presence update (op 3) */ 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); +int discord_reconnect(discord_t *d); /* RESUME if possible, else fresh IDENTIFY */ #endif \ No newline at end of file diff --git a/orbisrpc/jsonlite.c b/orbisrpc/jsonlite.c index d08c661..ba17d6a 100644 --- a/orbisrpc/jsonlite.c +++ b/orbisrpc/jsonlite.c @@ -147,7 +147,7 @@ void jl_free(jl_val_t *v){ free(v); } -const jl_val_t *jl_obj_get(const jl_val_t *obj, const char *key){ +jl_val_t *jl_obj_get(jl_val_t *obj, const char *key){ if(!obj||obj->type!=JL_OBJECT)return NULL; jl_val_t *p=obj->child; while(p){ @@ -160,7 +160,7 @@ const jl_val_t *jl_obj_get(const jl_val_t *obj, const char *key){ return NULL; } -const jl_val_t *jl_arr_at(const jl_val_t *arr, size_t i){ +jl_val_t *jl_arr_at(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; } @@ -209,11 +209,8 @@ static void emit(jl_val_t *v, char **out, size_t *cap, size_t *len){ first=0; escstr(p->str,out,cap,len); 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 { - size_t lz=*len+1; if(lz>*cap){*cap=lz*2;*out=realloc(*out,*cap);} (*out)[(*len)++]='n'; - } - /* for non-pair objects where a STRING key sits directly: handled above via child */ + if(p->child){ emit(p->child,out,cap,len); } + else { size_t lz=*len+4; if(lz>*cap){*cap=lz*2;*out=realloc(*out,*cap);} memcpy(*out+*len,"null",4);*len+=4; } } p=p->next; } l=*len+1; if(l>*cap){*cap=l*2;*out=realloc(*out,*cap);} (*out)[(*len)++]='}'; return; } } @@ -226,9 +223,9 @@ char *jl_stringify(const jl_val_t *v){ char *out=(char*)malloc(256); if(!out) return 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; + char *t=(char*)realloc(out,len+1); + if(t){ t[len]=0; return t; } + out[len]=0; return out; /* realloc failed: still return what we have */ } 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; } diff --git a/orbisrpc/jsonlite.h b/orbisrpc/jsonlite.h index 94439d5..7dfa703 100644 --- a/orbisrpc/jsonlite.h +++ b/orbisrpc/jsonlite.h @@ -16,8 +16,10 @@ typedef struct jl_val { typedef struct { const char *cur; const char *end; int err; } jl_parse_t; jl_val_t *jl_parse(const char *s, size_t len); void jl_free(jl_val_t *v); -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); +/* NOTE: the tree is mutable; getters return non-const so results can be + * passed to jl_obj_set / jl_arr_push without const-dropping warnings. */ +jl_val_t *jl_obj_get(jl_val_t *obj, const char *key); +jl_val_t *jl_arr_at(jl_val_t *arr, size_t i); char *jl_stringify(const jl_val_t *v); jl_val_t *jl_new_string(const char *s); jl_val_t *jl_new_number(double n); diff --git a/orbisrpc/log.c b/orbisrpc/log.c index 0ac3c4b..98426fb 100644 --- a/orbisrpc/log.c +++ b/orbisrpc/log.c @@ -1,13 +1,18 @@ -/* log.c */ +/* log.c - file logger: rotating (512 KB cap), timestamped, falls back to stderr. */ #include "log.h" #include #include #include +#include + +#define LOG_CAP (512 * 1024) static FILE *g_log = NULL; void log_init(const char *path) { if (g_log) return; + struct stat st; + if (stat(path, &st) == 0 && st.st_size > LOG_CAP) remove(path); /* rotate */ g_log = fopen(path, "ab"); if (g_log) setvbuf(g_log, NULL, _IONBF, 0); } @@ -19,7 +24,7 @@ void log_msg(const char *fmt, ...) { time_t t = time(NULL); struct tm *tm = localtime(&t); char ts[20]; - strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M", tm); + strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", tm); fprintf(g_log, "[%s] ", ts); vfprintf(g_log, fmt, ap); fprintf(g_log, "\n"); @@ -30,4 +35,4 @@ void log_msg(const char *fmt, ...) { void log_close(void) { if (g_log && g_log != stderr) fclose(g_log); g_log = NULL; -} +} \ No newline at end of file diff --git a/orbisrpc/main.c b/orbisrpc/main.c index 8092ab8..59edbd6 100644 --- a/orbisrpc/main.c +++ b/orbisrpc/main.c @@ -9,6 +9,7 @@ #include "b64.h" #include #include +#include #include #include #include @@ -41,17 +42,18 @@ static int ensure_token(void){ 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 */ + g_cfg.presence_state[0]?g_cfg.presence_state:"On PS4", /* state */ name, /* details = game name */ 1, 1, /* party */ - "orbisrpc:playing", /* large_image key (asset) */ + "orbisrpc: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/orbisRPC */ + mkdir(DATA_DIR, 0777); /* payloads may create their own dir */ + log_init(LOG_PATH); 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){ @@ -61,20 +63,20 @@ int main(int argc, char **argv){ discord_t dc; /* keep the daemon alive: transient OAuth/gateway failures retry instead * of exiting (a GoldHEN payload only runs once per boot) */ - for(;;){ + for(;;){ /* outer: token refresh + fresh connect */ if(ensure_token()!=0){ log_msg("no valid token; retry in %ds", g_cfg.poll_interval_s); sleep((unsigned)g_cfg.poll_interval_s); continue; } - if(discord_connect(&dc, g_cfg.access_token, NULL)!=0){ + if(discord_connect(&dc, g_cfg.access_token, 0)!=0){ log_msg("gateway connect failed; retry in %ds", g_cfg.poll_interval_s); sleep((unsigned)g_cfg.poll_interval_s); continue; } char last_name[128]=""; int64_t last_change = 0; - while(1){ + while(1){ /* inner: live session */ 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]){ @@ -92,8 +94,11 @@ int main(int argc, char **argv){ } if(discord_tick(&dc) != 0){ log_msg("gateway dropped; reconnecting..."); - ws_close(&dc.ws); - break; + if(discord_reconnect(&dc)!=0){ + ws_close(&dc.ws); + break; /* outer loop: full retry (token + connect) */ + } + last_name[0]=0; /* force re-push of the current game */ } usleep((useconds_t)(g_cfg.poll_interval_s * 1000000)); } @@ -102,4 +107,4 @@ int main(int argc, char **argv){ ws_close(&dc.ws); log_close(); return 0; -} +} \ No newline at end of file diff --git a/orbisrpc/ws.c b/orbisrpc/ws.c index 95186b9..12329de 100644 --- a/orbisrpc/ws.c +++ b/orbisrpc/ws.c @@ -102,7 +102,9 @@ int ws_connect(ws_t *w, const char *host, int port, const char *resource, const int on = 1; sceNetSetsockopt(fd, SOL_SOCKET, SO_NBIO, &on, sizeof on); w->fd = fd; w->sock = fd; w->connected = 1; w->nb = 1; - /* TLS via LibreSSL */ + /* TLS via LibreSSL. The socket is ALREADY non-blocking, so SSL_connect + * returns WANT_READ/WANT_WRITE instead of completing in one call: poll + * with a deadline until the handshake finishes or times out. */ SSL_CTX *ctx = SSL_CTX_new(SSLv23_client_method()); if(!ctx){ log_msg("SSL_CTX_new fail"); goto fail; } SSL *ssl = SSL_new(ctx); @@ -112,7 +114,17 @@ int ws_connect(ws_t *w, const char *host, int port, const char *resource, const * TLSEXT_NAMETYPE_host_name(0), hostname). Discord TLS needs the host. */ if(SSL_ctrl(ssl, 55, 0, (void *)host) != 1){ log_msg("SNI warn"); } SSL_set_connect_state(ssl); - if(SSL_connect(ssl) != 1){ log_msg("SSL_connect fail"); SSL_free(ssl); SSL_CTX_free(ctx); goto fail; } + int cr=-1; int64_t t0=time(NULL); + for(;;){ + cr = SSL_connect(ssl); + if(cr==1) break; + int e = SSL_get_error(ssl, cr); + if(e==SSL_ERROR_WANT_READ || e==SSL_ERROR_WANT_WRITE){ + if(time(NULL)-t0 > 5){ log_msg("SSL_connect timeout"); goto fail; } + usleep(20000); continue; + } + log_msg("SSL_connect fail err=%d", e); goto fail; + } w->ssl_ctx = (int32_t)(intptr_t)ctx; /* stash for teardown */ w->ssl = (int32_t)(intptr_t)ssl; /* HTTP Upgrade handshake */ @@ -121,22 +133,22 @@ int ws_connect(ws_t *w, const char *host, int port, const char *resource, const "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; } - /* read handshake response (blocking here is fine: server responds immediately). - * Socket is non-blocking, so handle WANT_READ with a deadline instead of - * busy-waiting forever if the server never answers. */ - char hdr[512]; int hlen=0, rd; int64_t t0=time(NULL); - while(hlen<(int)sizeof hdr-4){ - rd = SSL_read(ssl, hdr+hlen, 1); - if(rd==0 || rd<0){ - int e = SSL_get_error(ssl, rd); - if(e==SSL_ERROR_WANT_READ || e==SSL_ERROR_WANT_WRITE){ - if(time(NULL)-t0 > 5){ log_msg("hs timeout"); goto fail; } - usleep(20000); continue; - } - goto fail; /* EOF or real error before 101 */ + /* read handshake response in chunks (socket is non-blocking: handle + * WANT_READ with a deadline instead of busy-waiting forever). */ + char hdr[512]; int hlen=0, rd; t0=time(NULL); + while(hlen<(int)sizeof hdr-1){ + rd = SSL_read(ssl, hdr+hlen, (int)(sizeof hdr-1-hlen)); + if(rd>0){ + hlen+=rd; hdr[hlen]=0; + if(hlen>=4 && memcmp(hdr+hlen-4,"\r\n\r\n",4)==0) break; + continue; } - hlen++; hdr[hlen]=0; - if(hlen>=4 && memcmp(hdr+hlen-4,"\r\n\r\n",4)==0) break; + int e = SSL_get_error(ssl, rd); + if(e==SSL_ERROR_WANT_READ || e==SSL_ERROR_WANT_WRITE){ + if(time(NULL)-t0 > 5){ log_msg("hs timeout"); goto fail; } + usleep(20000); continue; + } + goto fail; /* EOF or real error before 101 */ } if(hlen<12 || !strstr(hdr,"101")){ log_msg("no 101: %.40s", hdr); goto fail; } log_msg("ws: connected (handshake ok)"); @@ -152,8 +164,8 @@ fail: /* --- send ----------------------------------------------------------- */ int ws_send_text(ws_t *w, const char *msg, size_t len){ if(!w->connected) return -1; + if(len > 4080){ log_msg("ws frame too big (%zu); refusing", len); return -1; } unsigned char frame[4096]; size_t f=0; - if(len > 4080) len = 4080; 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; } @@ -177,6 +189,13 @@ int ws_send_ping(ws_t *w){ return SSL_write((SSL*)(intptr_t)w->ssl, p, 2); } +/* RFC 6455: pong frames must be sent in reply to server pings. */ +int ws_send_pong(ws_t *w){ + if(!w->connected) return -1; + unsigned char p[2]={0x8A,0x00}; + return SSL_write((SSL*)(intptr_t)w->ssl, p, 2); +} + /* --- recv (non-blocking, buffered) ---------------------------------- */ /* Try to parse one complete server frame starting at rbuf[rpos]. * On success fills *fin and *opcode, copies payload into buf (truncated to cap), diff --git a/orbisrpc/ws.h b/orbisrpc/ws.h index f76d2c6..2468d0a 100644 --- a/orbisrpc/ws.h +++ b/orbisrpc/ws.h @@ -20,6 +20,7 @@ typedef struct { 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_send_pong(ws_t *w); /* Returns frame payload length (>0), 0 if no complete frame yet, <0 on error. * Server frames are unmasked (RFC 6455). */ int ws_recv_frame(ws_t *w, char *buf, size_t cap, int *opcode_out, int *fin_out);