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:
SirHumza committed 2026-08-14 20:24:24 +02:00
1 parent 90c86d9f1a
commit d40fc600c4
10 files changed
+231 -107

No files matched your search

+15 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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),
+1
View File
@@ -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);