mirror of
https://github.com/SirHumza/orbisRPC.git
synced 2026-10-25 15:24:40 +01:00
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.
This commit is contained in:
1 parent
90c86d9f1a
commit
d40fc600c4
10 files changed
+231
-107
No files matched your search
+15
-4
@@ -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);
|
||||
}
|
||||
+2
-1
@@ -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' && i<cap-1){ out[i++]=*p++; }
|
||||
out[i]=0;
|
||||
while(i>0 && (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;
|
||||
}
|
||||
+136
-54
@@ -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 <stdlib.h>
|
||||
#include <time.h>
|
||||
|
||||
#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<hcap-1){ host[hl++]=*p++; }
|
||||
host[hl]=0;
|
||||
*port=0;
|
||||
if(*p==':'){
|
||||
p++;
|
||||
while(*p>='0'&&*p<='9'){ *port=*port*10+(*p-'0'); p++; }
|
||||
}
|
||||
if(!*port) *port=443;
|
||||
if(*p=='/'){
|
||||
size_t rl=0;
|
||||
while(*p && rl<rcap-1){ res[rl++]=*p++; }
|
||||
res[rl]=0;
|
||||
} else {
|
||||
res[0]='/'; res[1]=0;
|
||||
}
|
||||
}
|
||||
|
||||
/* fetch the real gateway url (public endpoint, no bot auth needed) */
|
||||
static void fetch_gateway_url(char *url, size_t cap){
|
||||
strncpy(url, GATEWAY_DEFAULT, cap-1); url[cap-1]=0;
|
||||
char resp[512]="";
|
||||
if(http_get("https://discord.com/api/gateway",resp,sizeof resp)!=0){
|
||||
log_msg("gateway fetch failed; using default");
|
||||
return;
|
||||
}
|
||||
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);
|
||||
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;
|
||||
}
|
||||
+7
-6
@@ -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
|
||||
+7
-10
@@ -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; }
|
||||
|
||||
+4
-2
@@ -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);
|
||||
|
||||
+8
-3
@@ -1,13 +1,18 @@
|
||||
/* log.c */
|
||||
/* log.c - file logger: rotating (512 KB cap), timestamped, falls back to stderr. */
|
||||
#include "log.h"
|
||||
#include <stdarg.h>
|
||||
#include <time.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
}
|
||||
+14
-9
@@ -9,6 +9,7 @@
|
||||
#include "b64.h"
|
||||
#include <orbis/UserService.h>
|
||||
#include <orbis/libkernel.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+37
-18
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user