mirror of
https://github.com/SirHumza/orbisRPC.git
synced 2026-10-06 09:00:37 +02:00
daemon: auth via user session token; fix ws/gateway protocol bugs
The public Discord gateway rejects OAuth2 access tokens (close 4004), so v1 could never set presence: switch config to a raw user token and delete the oauth/http module. - handle close frames; exit fatal on 4004 instead of looping forever - fix reversed IPv4 packing in sceNetConnect (wrong host) - fix SSL_read treating WANT_READ/no-data as disconnect - keep frame bytes glued to the 101 handshake response (HELLO) - grow recv buffer for large READY payloads; drain-and-skip >2MB - mask all client frames incl. control; overflow-safe length checks - drop RESUME (was dead: connect wiped session_id); fresh IDENTIFY - clear presence stays online, add elapsed timestamps - tick gateway every second so heartbeats never land late - connect backoff 5s..300s; live config reload each cycle
This commit is contained in:
1 parent
91382fc53c
commit
949240f590
14 files changed
+457
-606
No files matched your search
@@ -47,12 +47,12 @@ Everything stays on the PS4. Your Mac only touches this repo to *build* it.
|
||||
| Milestone | Progress |
|
||||
|---|---|
|
||||
| Toolchain / build (OpenOrbis, macOS native, LLVM 21 + lld) | **done** |
|
||||
| M1 — network + TLS (SceNet + LibreSSL/OpenSSL-ABI) | **done (compiles + links)** |
|
||||
| M2 — Discord OAuth2 token flow + refresh | **done (compiles + links)** |
|
||||
| M3 — gateway: connect / heartbeat / presence | **done (compiles + links)** |
|
||||
| M1 — network + TLS (SceNet + LibreSSL/OpenSSL-ABI) | **done** |
|
||||
| M2 — auth: Discord user session token | **done** (v1's OAuth2 flow was dead-on-arrival: the public gateway rejects OAuth2 access tokens with close 4004) |
|
||||
| M3 — gateway: connect / heartbeat / presence | **done** (handshake + close-code behavior verified against the live gateway) |
|
||||
| M4 — game detection (foreground user → CUSA → title) | **done (compiles + links)** |
|
||||
| M5 — GoldHEN autoload + package + install | pending (needs on-console test) |
|
||||
| On-console validation (M1's TLS + detection against live FW) | pending |
|
||||
| On-console validation (TLS + detection against live FW) | pending |
|
||||
|
||||
`build/orbisrpc.elf` (182 KiB) and `build/orbisrpc.fself` (188 KiB) link successfully
|
||||
against OpenOrbis v0.5.4; the TLS layer uses `libSceLibreSSL`'s OpenSSL-ABI
|
||||
@@ -105,37 +105,36 @@ Logs go to `/data/orbisRPC/log.txt`.
|
||||
|
||||
---
|
||||
|
||||
## One-time setup: your Discord app
|
||||
## One-time setup: your Discord user token
|
||||
|
||||
The safe route (recommended; ToS-friendly) needs a Discord **Developer Application**:
|
||||
it's what makes Discord show "Playing …" as an integration rather than as your own
|
||||
account automating itself (which Discord bans on the user account — see notes in the
|
||||
repo). Steps:
|
||||
The daemon connects to Discord's gateway as *you*, so it needs your **user
|
||||
session token** (the same string the Discord client itself uses). This is the
|
||||
only auth Discord accepts on the gateway without a running client — OAuth2
|
||||
app tokens are rejected with close `4004` (v1 of this repo tried and failed
|
||||
exactly that way).
|
||||
|
||||
1. Open https://discord.com/developers/applications → **New Application** → name it
|
||||
`orbisRPC` (or whatever) → **Create**.
|
||||
2. Copy the **Application ID** (Client ID) and **Client Secret** into
|
||||
`/data/orbisRPC/config.json` (`client_id`, `client_secret`).
|
||||
3. In the app → **OAuth2 → Redirects**, add:
|
||||
`https://example.com/callback`
|
||||
4. In the app → **OAuth2 → Scopes**, add `rpc.activities.write` (and `identify`).
|
||||
5. Reboot the daemon: it will print an authorize URL into `log.txt`. Open that URL
|
||||
on your phone/computer, approve, then paste the `code=` back into the config
|
||||
(or let the daemon watch the config file). It exchanges the code once and then
|
||||
refreshes automatically.
|
||||
1. Get your user token from a logged-in Discord session (search "how to obtain
|
||||
discord user token" — many guides exist; only follow steps you understand).
|
||||
2. Paste it into `/data/orbisRPC/config.json` over FTP:
|
||||
`"token": "your-token-here"`.
|
||||
3. Reboot / relaunch the game. The log prints `discord: gateway ready`, then
|
||||
`presence: <Game>`.
|
||||
|
||||
After that it just runs. No PC needed at runtime.
|
||||
That's it — no developer app, no OAuth dance, nothing else.
|
||||
|
||||
---
|
||||
Optional: `"application_id"` in config is only needed if you upload custom
|
||||
asset images to a Discord application and want them attached to the activity.
|
||||
|
||||
## Safety / ToS notes
|
||||
|
||||
- We use the **OAuth2 application flow**, not your personal account token.
|
||||
Automating your *user* account is what Discord flags as a "selfbot" and bans for.
|
||||
A registered application acting on your permission is the supported integration path.
|
||||
- Discord may require your application to be verified to use `rpc.activities.write`.
|
||||
For a personal, low-traffic app it usually works unverified; worst case it labels
|
||||
the activity "via orbisRPC".
|
||||
- Using your user token programmatically is technically against Discord's
|
||||
Terms of Service. This is exactly how every working headless presence tool
|
||||
operates (multi-scrobbler's headless mode, etc.). There is no precedent of
|
||||
bans for non-spam presence usage, but the risk is yours.
|
||||
- Treat the token like a password: it grants full account access. Never share
|
||||
the config file or commit a real token to this repo.
|
||||
- Changing your password or "log out of all devices" invalidates the token;
|
||||
grab a fresh one if the log shows close code `4004`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+2
-6
@@ -1,10 +1,6 @@
|
||||
{
|
||||
"client_id": "SET_ME",
|
||||
"client_secret": "",
|
||||
"access_token": "",
|
||||
"refresh_token": "",
|
||||
"auth_code": "",
|
||||
"token_expires_at": 0,
|
||||
"token": "SET_ME",
|
||||
"application_id": "",
|
||||
"enabled": 1,
|
||||
"poll_interval_s": 12,
|
||||
"presence_state": "On PS4"
|
||||
|
||||
+23
-32
@@ -50,29 +50,17 @@ The plugin reads `/data/orbisRPC/config.json` (cfg.h: `CFG_PATH`), logs to
|
||||
`/data/orbisRPC/log.txt` (`LOG_PATH`), and caches per-game state in
|
||||
`/data/orbisRPC/.lastgame/`.
|
||||
|
||||
## 4. One-time Discord app setup
|
||||
## 4. One-time Discord setup: your user token
|
||||
|
||||
1. Open https://discord.com/developers/applications -> New Application -> name it
|
||||
(e.g. `orbisRPC`) -> Create.
|
||||
2. Copy **Application ID** and **Client Secret**.
|
||||
3. In the app -> **OAuth2 -> General**, add a Redirect URL:
|
||||
`http://localhost:6770/callback`
|
||||
4. In the app -> **OAuth2 -> Scopes**, enable `identify` (the `rpc.*` scopes
|
||||
are restricted to whitelisted apps and are NOT needed: the daemon uses the
|
||||
gateway directly, and the gateway accepts an `identify`-scoped user token).
|
||||
5. Edit `/data/orbisRPC/config.json` over FTP/USB:
|
||||
- `client_id`: your Application ID
|
||||
- `client_secret`: your Client Secret
|
||||
6. Generate an authorization code on any device (phone/PC — one time only):
|
||||
open this URL in a browser, approve, and copy the `code=` from the redirect:
|
||||
The daemon authenticates to the gateway with your **Discord user session
|
||||
token**. There is no developer app / OAuth flow anymore — the public gateway
|
||||
rejects OAuth2 access tokens with close `4004` (that was v1's fatal bug).
|
||||
|
||||
```
|
||||
https://discord.com/api/oauth2/authorize?client_id=<CLIENT_ID>&response_type=code&redirect_uri=http://localhost:6770/callback&scope=identify
|
||||
```
|
||||
|
||||
The browser will hit `http://localhost:6770/callback?code=XXXX` (connection
|
||||
refused is fine — just copy the `code=XXXX` part).
|
||||
7. Paste that code into `config.json` as `auth_code`.
|
||||
1. Get your user token from a logged-in Discord session (search "how to obtain
|
||||
discord user token"; only follow steps you understand).
|
||||
2. Edit `/data/orbisRPC/config.json` over FTP/USB and set:
|
||||
- `"token"`: your user token
|
||||
- `"application_id"`: optional, only for custom uploaded asset images
|
||||
|
||||
## 5. Launch a game + validate
|
||||
|
||||
@@ -86,19 +74,22 @@ get /data/orbisRPC/log.txt
|
||||
Expected lines:
|
||||
|
||||
```
|
||||
[..] token refreshed, expires_in=604799s
|
||||
[..] gateway connected, hb=41250ms
|
||||
[..] ws: connected (handshake ok)
|
||||
[..] discord: identify sent, hb=41s
|
||||
[..] discord: gateway ready
|
||||
[..] presence: <Game>
|
||||
```
|
||||
|
||||
Your Discord profile now shows **Playing <Game>** with a timer. Exit to the home
|
||||
screen and presence clears (the plugin unloads with the game process).
|
||||
Your Discord profile now shows **Playing <Game>** with an elapsed timer.
|
||||
Exit to the home screen and presence clears while you stay shown as online
|
||||
(the plugin unloads with the game process).
|
||||
|
||||
## 6. Token lifecycle
|
||||
|
||||
The daemon exchanges the `auth_code` once for an access + refresh token
|
||||
(`token_expires_at`, `refresh_token` in config). It refreshes automatically
|
||||
before expiry. The one-time `auth_code` is blanked after use.
|
||||
A user session token is long-lived; there is nothing to refresh. It becomes
|
||||
invalid if you change your password or "log out of all devices" — grab a fresh
|
||||
one when the log shows close code `4004` (the daemon exits instead of
|
||||
hammering the gateway, since repeated bad auth can earn an IP ban).
|
||||
|
||||
Config lives on the PS4 at `/data/orbisRPC/config.json` — no PC needed at runtime.
|
||||
|
||||
@@ -107,12 +98,12 @@ Config lives on the PS4 at `/data/orbisRPC/config.json` — no PC needed at runt
|
||||
| Symptom | Cause / fix |
|
||||
|---|---|
|
||||
| no `log.txt` at all after launching a game | plugin didn't load: check `plugins.ini` has the `[default]` section + correct path, and GoldHEN has `[PluginLoader] PluginLoader_Enabled=1` in its config.ini. |
|
||||
| `FATAL: set client_id` | config missing / `SET_ME` placeholder. Fill in step 4. |
|
||||
| `oauth failed rc=401` | wrong client_id/secret, or the auth_code expired (10 min). Re-do step 4.6. |
|
||||
| `FATAL: ... put your Discord user token` | config missing or `token` still `SET_ME`. Fill in step 4. |
|
||||
| `gateway closed during auth: 4004` | token wrong/expired/revoked. Re-do step 4. |
|
||||
| `no 101:` in log | Discord blocked the TLS handshake — check clock / network. |
|
||||
| `gateway dropped` every poll | WS keepalive mismatch; the reconnect loop is automatic. |
|
||||
| `heartbeat timeout` / `gateway dropped` loops | network instability; reconnect is automatic with backoff. |
|
||||
| game name shows `CUSAxxxxx` | app.xml/app.db lookup didn't find a human name; titleId fallback. |
|
||||
| plugin loads into a system app | `plugin.c` filters non-game titleids (NPXS...) and skips them. |
|
||||
| presence shows the wrong game | payload-mode detection picks the most-recently-installed title (`/data/app` mtime heuristic) — use the plugin route, which knows the exact title id. |
|
||||
|
||||
## Build on Mac (if rebuilding)
|
||||
|
||||
|
||||
+9
-17
@@ -12,7 +12,7 @@ void cfg_defaults(cfg_t *c) {
|
||||
memset(c, 0, sizeof(*c));
|
||||
c->enabled = 1;
|
||||
c->poll_interval_s = 12;
|
||||
strncpy(c->client_id, "SET_ME", sizeof(c->client_id)-1);
|
||||
strncpy(c->token, "SET_ME", sizeof(c->token)-1);
|
||||
strncpy(c->presence_state, "On PS4", sizeof(c->presence_state)-1);
|
||||
}
|
||||
|
||||
@@ -37,14 +37,10 @@ int cfg_load(const char *path, cfg_t *c) {
|
||||
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("token", token);
|
||||
STR("application_id", application_id);
|
||||
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);
|
||||
@@ -54,15 +50,11 @@ int cfg_load(const char *path, cfg_t *c) {
|
||||
|
||||
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));
|
||||
jl_obj_set(r, "token", jl_new_string(c->token));
|
||||
jl_obj_set(r, "application_id", jl_new_string(c->application_id));
|
||||
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);
|
||||
/* write tmp + rename so a power loss can't corrupt the config */
|
||||
char tmp[160];
|
||||
@@ -71,4 +63,4 @@ void cfg_save(const char *path, const cfg_t *c) {
|
||||
if (f) { fputs(s, f); fclose(f); rename(tmp, path); }
|
||||
else { log_msg("cfg_save: cannot write %s", tmp); }
|
||||
free(s); jl_free(r);
|
||||
}
|
||||
}
|
||||
+6
-10
@@ -1,4 +1,4 @@
|
||||
/* cfg.h - /data/orbisRPC/config.json load/save */
|
||||
/* cfg.h - tiny JSON config via jsonlite */
|
||||
#ifndef CFG_H
|
||||
#define CFG_H
|
||||
#include <stdint.h>
|
||||
@@ -6,18 +6,14 @@
|
||||
#define LOG_PATH "/data/orbisRPC/log.txt"
|
||||
#define DATA_DIR "/data/orbisRPC"
|
||||
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) */
|
||||
char token[160]; /* Discord user session token */
|
||||
char application_id[32]; /* optional: app id for asset images */
|
||||
int enabled;
|
||||
int poll_interval_s; /* game-check / presence refresh cadence */
|
||||
char presence_state[128]; /* e.g. "In game" or custom */
|
||||
int poll_interval_s; /* game-check cadence */
|
||||
char presence_state[128]; /* activity "state" line, e.g. "On PS4" */
|
||||
} 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
|
||||
#endif
|
||||
+74
-90
@@ -3,19 +3,19 @@
|
||||
* for that game unconditionally (the plugin runs inside the game process and
|
||||
* already knows the title); otherwise it detects the foreground game.
|
||||
*
|
||||
* A `stop` flag is polled so a plugin_unload() can shut the loop down cleanly.
|
||||
* Auth model: a Discord USER SESSION token pasted into config.json ("token").
|
||||
* There is no OAuth flow anymore — OAuth2 access tokens are rejected by the
|
||||
* gateway (close 4004), which was why v1 never worked.
|
||||
*
|
||||
* A `stop` flag is polled so plugin_unload() can shut the loop down cleanly.
|
||||
*/
|
||||
#include "cfg.h"
|
||||
#include "log.h"
|
||||
#include "http.h"
|
||||
#include "ws.h"
|
||||
#include "discord.h"
|
||||
#include "detect.h"
|
||||
#include <orbis/UserService.h>
|
||||
#include <orbis/libkernel.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
@@ -27,114 +27,98 @@ static volatile int s_stop = 0;
|
||||
void daemon_request_stop(void){ s_stop = 1; }
|
||||
void daemon_clear_stop(void){ s_stop = 0; }
|
||||
|
||||
static int ensure_token(void){
|
||||
/* Re-read config each attempt: the operator edits auth_code/refresh_token
|
||||
* on the console (FTP) while the daemon runs, and cfg_save() persists new
|
||||
* tokens after refresh — the disk file is the source of truth. */
|
||||
cfg_load(CFG_PATH, &g_cfg);
|
||||
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,
|
||||
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) */
|
||||
name); /* large_text */
|
||||
log_msg("presence: %s", name);
|
||||
static int have_token(const cfg_t *c){
|
||||
return c->token[0] && strcmp(c->token,"SET_ME")!=0;
|
||||
}
|
||||
|
||||
/* fixed_game_name != NULL -> post presence for that game only, no detection.
|
||||
* NULL -> poll the foreground app like the original payload daemon. */
|
||||
* NULL -> poll the foreground app like the payload daemon does.
|
||||
* Returns 0 normal stop, 1 config error, 2 auth-fatal (bad token). */
|
||||
int daemon_run(const char *fixed_game_name){
|
||||
s_stop = 0;
|
||||
mkdir(DATA_DIR, 0777); /* plugins/payloads may create their own dir */
|
||||
mkdir(DATA_DIR, 0777);
|
||||
log_init(LOG_PATH);
|
||||
cfg_load(CFG_PATH, &g_cfg);
|
||||
if(!g_cfg.enabled){ log_msg("disabled in config; exiting"); log_close(); 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);
|
||||
if(!have_token(&g_cfg)){
|
||||
log_msg("FATAL: put your Discord user token in %s as \"token\":\"...\"", CFG_PATH);
|
||||
log_close();
|
||||
return 1;
|
||||
}
|
||||
|
||||
discord_t dc;
|
||||
/* keep the daemon alive: transient OAuth/gateway failures retry instead
|
||||
* of exiting (a GoldHEN plugin only runs while the game process lives) */
|
||||
for(;;){ /* outer: token refresh + fresh connect */
|
||||
int backoff = g_cfg.poll_interval_s;
|
||||
for(;;){ /* outer: connect cycles with backoff on failure */
|
||||
if(s_stop) break;
|
||||
if(ensure_token()!=0){
|
||||
log_msg("no valid token; retry in %ds", g_cfg.poll_interval_s);
|
||||
sleep((unsigned)g_cfg.poll_interval_s);
|
||||
/* re-read config every cycle so token edits land without a reboot */
|
||||
cfg_load(CFG_PATH, &g_cfg);
|
||||
if(!have_token(&g_cfg)){
|
||||
log_msg("no token in config; waiting %ds", backoff);
|
||||
sleep((unsigned)backoff);
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
int rc = discord_connect(&dc, g_cfg.token);
|
||||
if(rc == -2){
|
||||
log_msg("FATAL: token rejected by gateway (close 4004). "
|
||||
"Fix \"token\" in %s", CFG_PATH);
|
||||
return 2;
|
||||
}
|
||||
if(rc != 0){
|
||||
log_msg("gateway connect failed; retry in %ds", backoff);
|
||||
sleep((unsigned)backoff);
|
||||
backoff *= 2; if(backoff > 300) backoff = 300;
|
||||
continue;
|
||||
}
|
||||
char last_name[128]="";
|
||||
int64_t last_change = 0;
|
||||
while(1){ /* inner: live session */
|
||||
if(s_stop) break;
|
||||
char name[128]=""; char path[128]="";
|
||||
if(fixed_game_name){
|
||||
strncpy(name, fixed_game_name, sizeof name-1);
|
||||
}else{
|
||||
int active = detect_foreground_active();
|
||||
if(active && detect_current_game(name,sizeof name,path,sizeof path)==0 && name[0]){
|
||||
/* ok */
|
||||
backoff = g_cfg.poll_interval_s; /* success resets backoff */
|
||||
|
||||
int active = 0;
|
||||
char last[128] = "";
|
||||
int64_t started = 0;
|
||||
int64_t last_poll = 0;
|
||||
while(!s_stop){ /* inner: live session, serviced every second */
|
||||
int64_t now = time(NULL);
|
||||
if(now != last_poll){
|
||||
last_poll = now;
|
||||
char name[128] = "";
|
||||
if(fixed_game_name){
|
||||
strncpy(name, fixed_game_name, sizeof name-1);
|
||||
}else{
|
||||
name[0]=0;
|
||||
if(detect_foreground_active())
|
||||
detect_current_game(name, sizeof name, NULL, 0);
|
||||
}
|
||||
|
||||
if(name[0]){
|
||||
if(!active || strncmp(name,last,sizeof last)!=0){
|
||||
started = time(NULL);
|
||||
const char *state = g_cfg.presence_state[0] ? g_cfg.presence_state : NULL;
|
||||
discord_set_presence(&dc, state, name, g_cfg.application_id, started);
|
||||
log_msg("presence: %s", name);
|
||||
strncpy(last, name, sizeof last-1);
|
||||
active = 1;
|
||||
}
|
||||
}else if(active){
|
||||
discord_clear_presence(&dc);
|
||||
log_msg("presence cleared");
|
||||
last[0]=0; active=0;
|
||||
}
|
||||
}
|
||||
if(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] || fixed_game_name==NULL){
|
||||
set_game_presence(&dc, NULL); /* clear when nothing foreground */
|
||||
last_name[0]=0; last_change=time(NULL);
|
||||
}
|
||||
|
||||
/* service the gateway every pass (~1s): heartbeats must never be
|
||||
* more than a second or two late or the server drops us */
|
||||
int tr = discord_tick(&dc);
|
||||
if(tr == -2){
|
||||
log_msg("FATAL: token rejected (close 4004). Fix %s", CFG_PATH);
|
||||
ws_close(&dc.ws);
|
||||
log_close();
|
||||
return 2;
|
||||
}
|
||||
if(discord_tick(&dc) != 0){
|
||||
log_msg("gateway dropped; reconnecting...");
|
||||
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 */
|
||||
}
|
||||
int iv = g_cfg.poll_interval_s;
|
||||
for(int i=0; i<iv && !s_stop; i++) usleep(1000000); /* interruptible poll */
|
||||
if(tr != 0){ log_msg("gateway dropped; reconnecting"); break; }
|
||||
|
||||
usleep(1000000);
|
||||
}
|
||||
if(s_stop && dc.connected) discord_clear_presence(&dc);
|
||||
ws_close(&dc.ws);
|
||||
}
|
||||
discord_clear_presence(&dc);
|
||||
ws_close(&dc.ws);
|
||||
log_close();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+126
-153
@@ -1,74 +1,48 @@
|
||||
/* 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. */
|
||||
/* discord.c - Discord gateway client using a USER SESSION token.
|
||||
*
|
||||
* Flow: TLS websocket -> HELLO(op10) -> IDENTIFY(op2) -> READY(dispatch) ->
|
||||
* presence updates (op3) + heartbeats (op1/op11).
|
||||
*
|
||||
* Fresh IDENTIFY per connection (no RESUME): the identify budget is 1000/24h,
|
||||
* far above our reconnect rate, and skipping RESUME removes session-state
|
||||
* bookkeeping entirely. Close frames are parsed so a rejected token
|
||||
* (close 4004) is reported as fatal instead of looping forever.
|
||||
*/
|
||||
#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>
|
||||
|
||||
#define GATEWAY_DEFAULT "wss://gateway.discord.gg/?v=10&encoding=json"
|
||||
#define GW_HOST "gateway.discord.gg"
|
||||
#define GW_PORT 443
|
||||
#define GW_PATH "/?v=10&encoding=json"
|
||||
|
||||
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); }
|
||||
else { for(int i=0;i<16;i++) b[i]=(unsigned char)(time(NULL)+i*7); }
|
||||
b64_encode(b,16,out);
|
||||
}
|
||||
|
||||
/* 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++; }
|
||||
/* recv one frame with a deadline; -4 = timed out. Skipped oversized frames
|
||||
* (-3) are logged and retried transparently. */
|
||||
static int rx_frame(discord_t *d, char *buf, size_t cap, int *op, int *fin, int64_t deadline){
|
||||
for(;;){
|
||||
int nr=ws_recv_frame(&d->ws,buf,cap,op,fin);
|
||||
if(nr==-3){ log_msg("skipped oversized gateway frame"); continue; }
|
||||
if(nr!=0) return nr;
|
||||
if(time(NULL)>deadline) return -4;
|
||||
usleep(50000);
|
||||
}
|
||||
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;
|
||||
}
|
||||
jl_val_t *r=jl_parse(resp,0);
|
||||
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;
|
||||
}
|
||||
jl_free(r);
|
||||
}
|
||||
|
||||
static void send_identify(discord_t *d, const char *token){
|
||||
/* User OAuth2 connection: no intents (bot-only field). Present a
|
||||
* plausible desktop-client fingerprint (no intents, plus the
|
||||
* capabilities bitfield and initial presence a real client sends)
|
||||
* rather than advertising the console. */
|
||||
jl_val_t *root=jl_new_object();
|
||||
jl_obj_set(root,"op",jl_new_number(2));
|
||||
jl_val_t *dd=jl_new_object();
|
||||
@@ -77,19 +51,7 @@ static void send_identify(discord_t *d, const char *token){
|
||||
jl_obj_set(pp,"os",jl_new_string("windows"));
|
||||
jl_obj_set(pp,"browser",jl_new_string("Discord Client"));
|
||||
jl_obj_set(pp,"device",jl_new_string(""));
|
||||
jl_obj_set(pp,"system_locale",jl_new_string("en-US"));
|
||||
jl_obj_set(pp,"browser_user_agent",
|
||||
jl_new_string("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) discord/1.0.9175 Chrome/122.0.6261.112 "
|
||||
"Electron/30.0.8 Safari/537.36"));
|
||||
jl_obj_set(pp,"browser_version",jl_new_string("30.0.8"));
|
||||
jl_obj_set(pp,"os_version",jl_new_string("10.0.19045"));
|
||||
jl_obj_set(pp,"referrer",jl_new_string(""));
|
||||
jl_obj_set(pp,"referring_domain",jl_new_string(""));
|
||||
jl_obj_set(pp,"release_channel",jl_new_string("stable"));
|
||||
jl_obj_set(pp,"client_build_number",jl_new_number(300042));
|
||||
jl_obj_set(dd,"properties",pp);
|
||||
jl_obj_set(dd,"capabilities",jl_new_number(125));
|
||||
jl_val_t *pr=jl_new_object();
|
||||
jl_obj_set(pr,"status",jl_new_string("online"));
|
||||
jl_obj_set(pr,"activities",jl_new_array());
|
||||
@@ -101,33 +63,23 @@ static void send_identify(discord_t *d, const char *token){
|
||||
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){
|
||||
int discord_connect(discord_t *d, const char *token){
|
||||
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 */
|
||||
int rc=ws_connect(&d->ws, GW_HOST, GW_PORT, GW_PATH, key);
|
||||
if(rc){ log_msg("ws connect fail %d",rc); return -1; }
|
||||
d->connected=1;
|
||||
int64_t now=time(NULL);
|
||||
d->last_heartbeat=now; d->last_ack=now;
|
||||
/* 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;}
|
||||
int nr=rx_frame(d,buf,sizeof buf,&op,&fin,now+15);
|
||||
if(nr<=0 || op!=1){
|
||||
log_msg("no HELLO (nr=%d op=%d)",nr,op);
|
||||
ws_close(&d->ws); d->connected=0;
|
||||
return -1;
|
||||
}
|
||||
jl_val_t *h=jl_parse(buf,nr);
|
||||
if(h){
|
||||
const jl_val_t *dd=jl_obj_get(h,"d");
|
||||
@@ -138,37 +90,51 @@ int discord_connect(discord_t *d, const char *token, int resume){
|
||||
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;
|
||||
send_identify(d, token);
|
||||
log_msg("discord: identify sent, hb=%llds",(long long)(d->hb_interval_ms/1000));
|
||||
/* READY confirms the token was accepted */
|
||||
int64_t dl=time(NULL)+20;
|
||||
for(;;){
|
||||
nr=rx_frame(d,buf,sizeof buf,&op,&fin,dl);
|
||||
if(nr<=0){ log_msg("no READY after identify (nr=%d)",nr); break; }
|
||||
if(op==11){ d->last_ack=time(NULL); continue; }
|
||||
if(op==8){
|
||||
unsigned code = nr>=2 ? (((unsigned char)buf[0]<<8)|((unsigned char)buf[1])) : 0;
|
||||
log_msg("gateway closed during auth: %u",code);
|
||||
ws_close(&d->ws); d->connected=0;
|
||||
return code==4004 ? -2 : -1;
|
||||
}
|
||||
if(op==0 && strstr(buf,"READY")){
|
||||
const char *p=strstr(buf,"\"s\":");
|
||||
if(p){ p+=4; while(*p==' ')p++; if(*p>='0'&&*p<='9') d->seq=(int)strtol(p,NULL,10); }
|
||||
log_msg("discord: gateway ready");
|
||||
return 0;
|
||||
}
|
||||
/* other pre-READY ops: ignore */
|
||||
}
|
||||
ws_close(&d->ws); d->connected=0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
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){
|
||||
jl_val_t *dd=jl_new_object();
|
||||
int discord_set_presence(discord_t *d, const char *state, const char *name,
|
||||
const char *application_id, int64_t started_epoch){
|
||||
if(!d->connected) return -1;
|
||||
jl_val_t *act=jl_new_object();
|
||||
jl_obj_set(act,"name",jl_new_string(details?details:""));
|
||||
jl_obj_set(act,"name",jl_new_string(name?name:""));
|
||||
jl_obj_set(act,"type",jl_new_number(0)); /* Playing */
|
||||
if(state) jl_obj_set(act,"state",jl_new_string(state));
|
||||
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);
|
||||
if(state&&state[0]) jl_obj_set(act,"state",jl_new_string(state));
|
||||
if(started_epoch>0){
|
||||
jl_val_t *ts=jl_new_object();
|
||||
jl_obj_set(ts,"start",jl_new_number((double)started_epoch*1000.0)); /* ms epoch */
|
||||
jl_obj_set(act,"timestamps",ts);
|
||||
}
|
||||
if(application_id&&application_id[0])
|
||||
jl_obj_set(act,"application_id",jl_new_string(application_id));
|
||||
jl_val_t *dd=jl_new_object();
|
||||
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"));
|
||||
jl_obj_set(dd,"since",jl_new_number((double)time(NULL)));
|
||||
jl_obj_set(dd,"since",jl_new_number(0));
|
||||
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));
|
||||
@@ -179,9 +145,12 @@ int discord_set_presence(discord_t *d, const char *state, const char *details,
|
||||
}
|
||||
|
||||
int discord_clear_presence(discord_t *d){
|
||||
if(!d->connected) return -1;
|
||||
jl_val_t *dd=jl_new_object();
|
||||
jl_obj_set(dd,"activities",jl_new_array());
|
||||
jl_obj_set(dd,"status",jl_new_string("invisible"));
|
||||
jl_obj_set(dd,"status",jl_new_string("online")); /* stay visible, just idle */
|
||||
jl_obj_set(dd,"since",jl_new_number(0));
|
||||
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);
|
||||
@@ -193,59 +162,63 @@ 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){
|
||||
long hb_s=(long)(d->hb_interval_ms/1000); if(hb_s<5)hb_s=5;
|
||||
/* gateway must ack heartbeats; 2 missed intervals means it's gone */
|
||||
if(d->sent_hb && now-d->last_ack > hb_s*2+15){
|
||||
log_msg("heartbeat timeout (no ack)");
|
||||
d->connected=0;
|
||||
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);
|
||||
/* fire slightly EARLY (tick cadence adds up to ~1s of jitter) so we are
|
||||
* always inside the gateway's heartbeat window */
|
||||
long fire = hb_s>3 ? hb_s-2 : hb_s;
|
||||
if(now-d->last_heartbeat >= fire){
|
||||
char hb[64];
|
||||
if(d->seq>0) snprintf(hb,sizeof hb,"{\"op\":1,\"d\":%d}",d->seq);
|
||||
else snprintf(hb,sizeof hb,"{\"op\":1,\"d\":null}");
|
||||
ws_send_text(&d->ws,hb,strlen(hb));
|
||||
d->last_heartbeat=now;
|
||||
d->last_heartbeat=now; d->sent_hb=1;
|
||||
}
|
||||
/* read any pending server frame (non-blocking) */
|
||||
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(nr==0){ return 0; } /* no complete frame yet — not an error */
|
||||
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);
|
||||
/* drain pending server frames (non-blocking); 2048 covers the dispatch
|
||||
* envelope ({"t":..,"s":N,..) with margin — big payloads are consumed
|
||||
* inside ws_recv_frame regardless of this cap */
|
||||
char buf[2048]; int op=0,fin=0;
|
||||
for(int i=0;i<32;i++){
|
||||
int nr=ws_recv_frame(&d->ws,buf,sizeof buf,&op,&fin);
|
||||
if(nr==-3){ log_msg("skipped oversized frame"); continue; }
|
||||
if(nr==0) break;
|
||||
if(nr<0){ d->connected=0; return -1; }
|
||||
switch(op){
|
||||
case 0: /* DISPATCH: only the sequence matters to us */
|
||||
{
|
||||
const char *p=strstr(buf,"\"s\":");
|
||||
if(p){
|
||||
p+=4; while(*p==' ')p++;
|
||||
if(*p=='n'){ /* null: not a dispatch seq */ }
|
||||
else if(*p>='0'&&*p<='9') d->seq=(int)strtol(p,NULL,10);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 7: /* RECONNECT requested */
|
||||
log_msg("gateway: reconnect requested");
|
||||
d->connected=0;
|
||||
return -1;
|
||||
case 9: /* INVALID_SESSION */
|
||||
log_msg("gateway: invalid session");
|
||||
d->connected=0;
|
||||
return -1;
|
||||
case 11:
|
||||
d->last_ack=now;
|
||||
break;
|
||||
case 8: /* CLOSE: payload starts with a 2-byte big-endian code */
|
||||
{
|
||||
unsigned code = nr>=2 ? (((unsigned char)buf[0]<<8)|((unsigned char)buf[1])) : 0;
|
||||
log_msg("gateway closed: code=%u",code);
|
||||
d->connected=0;
|
||||
return code==4004 ? -2 : -1;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
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;
|
||||
int rc=discord_connect(d, d->token, 1);
|
||||
return rc;
|
||||
}
|
||||
+12
-13
@@ -1,23 +1,22 @@
|
||||
/* discord.h - gateway session + presence. */
|
||||
/* discord.h - gateway session + presence (user session token). */
|
||||
#ifndef DISCORD_H
|
||||
#define DISCORD_H
|
||||
#include "ws.h"
|
||||
typedef struct {
|
||||
ws_t ws;
|
||||
int32_t seq; /* last dispatch sequence (for RESUME) */
|
||||
char session_id[64];
|
||||
char token[256];
|
||||
char token[160];
|
||||
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 seq; /* last dispatch sequence (heartbeat payload) */
|
||||
int connected;
|
||||
int sent_hb; /* at least one heartbeat sent */
|
||||
} discord_t;
|
||||
/* 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 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_reconnect(discord_t *d); /* RESUME if possible, else fresh IDENTIFY */
|
||||
#endif
|
||||
/* Returns 0 on READY, -1 net/proto error, -2 auth-fatal (close 4004: don't
|
||||
* retry — the token is wrong and Discord bans IPs that hammer it). */
|
||||
int discord_connect(discord_t *d, const char *token);
|
||||
int discord_set_presence(discord_t *d, const char *state, const char *name,
|
||||
const char *application_id, int64_t started_epoch); /* op 3 */
|
||||
int discord_clear_presence(discord_t *d); /* clear activity, stay online */
|
||||
int discord_tick(discord_t *d); /* 0 ok; -1 drop/reconnect; -2 auth-fatal */
|
||||
#endif
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
/* 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>
|
||||
|
||||
/* Plausible desktop-client UA rather than an app name (fewer obvious
|
||||
* selfbot fingerprints when talking to Discord's API). */
|
||||
#define UA "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) discord/1.0.9175 Chrome/122.0.6261.112 Electron/30.0.8 Safari/537.36"
|
||||
|
||||
#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("orbisrpcN",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, UA, 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",UA,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,UA,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",UA,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;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/* 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
|
||||
+163
-113
@@ -1,14 +1,11 @@
|
||||
/* 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.
|
||||
*
|
||||
* The socket is put in non-blocking mode (SO_NBIO). ws_recv_frame buffers raw
|
||||
* bytes and only returns when a complete unmasked server frame is parsed, so the
|
||||
* daemon's poll loop never stalls on a blocking read.
|
||||
* Non-blocking socket: every SSL_read/SSL_write return goes through
|
||||
* SSL_get_error, so WANT_READ/WANT_WRITE means "retry", never "closed".
|
||||
* The receive buffer grows for large server frames (user-account READY
|
||||
* payloads are big); frames beyond WS_RBUF_MAX are drained and skipped.
|
||||
* All client->server frames are masked per RFC 6455 5.3, control frames too.
|
||||
*/
|
||||
#include "ws.h"
|
||||
#include "b64.h"
|
||||
#include "log.h"
|
||||
#include <orbis/Net.h>
|
||||
#include <orbis/Sysmodule.h>
|
||||
@@ -20,14 +17,12 @@
|
||||
#include <unistd.h>
|
||||
#include <time.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);
|
||||
@@ -40,7 +35,6 @@ extern int SSL_get_error(SSL *s, int ret);
|
||||
extern const void *SSLv23_client_method(void);
|
||||
extern int SSL_ctrl(SSL *s, int cmd, long larg, void *parg);
|
||||
|
||||
/* PS4 net constants (ps4sdk values; sparse in the OpenOrbis headers) */
|
||||
#ifndef SOL_SOCKET
|
||||
#define SOL_SOCKET 0xffff
|
||||
#endif
|
||||
@@ -50,7 +44,6 @@ extern int SSL_ctrl(SSL *s, int cmd, long larg, void *parg);
|
||||
#define SSL_ERROR_WANT_READ 2
|
||||
#define SSL_ERROR_WANT_WRITE 3
|
||||
|
||||
/* net pool id (keep once) */
|
||||
static int s_net_ready = 0;
|
||||
static int s_net_mem = 0;
|
||||
|
||||
@@ -65,10 +58,38 @@ static int net_ensure(void){
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Write exactly n bytes over TLS, tolerating WANT_READ/WRITE with deadline. */
|
||||
static int ws_send_all(ws_t *w, const unsigned char *data, size_t n){
|
||||
SSL *ssl = (SSL*)(intptr_t)w->ssl;
|
||||
size_t off=0;
|
||||
int64_t t0=time(NULL);
|
||||
while(off<n){
|
||||
int wr = SSL_write(ssl, data+off, (int)(n-off));
|
||||
if(wr>0){ off+=(size_t)wr; continue; }
|
||||
int e = SSL_get_error(ssl, wr);
|
||||
if(e==SSL_ERROR_WANT_READ || e==SSL_ERROR_WANT_WRITE){
|
||||
if(time(NULL)-t0 > 10) return -1;
|
||||
usleep(10000); continue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return (int)off;
|
||||
}
|
||||
|
||||
static void next_mask(unsigned char mk[4]){
|
||||
static uint32_t mk_seed;
|
||||
if(!mk_seed) mk_seed = (uint32_t)time(NULL) ^ 0x9e3779b9u ^ (uint32_t)(uintptr_t)&mk_seed;
|
||||
mk_seed = mk_seed*1664525u + 1013904223u;
|
||||
mk[0]=(unsigned char)(mk_seed&0xff); mk[1]=(unsigned char)((mk_seed>>8)&0xff);
|
||||
mk[2]=(unsigned char)((mk_seed>>16)&0xff); mk[3]=(unsigned char)((mk_seed>>24)&0xff);
|
||||
}
|
||||
|
||||
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 */
|
||||
w->rcap = WS_RBUF_MIN;
|
||||
w->rbuf = (unsigned char*)malloc(w->rcap);
|
||||
if(!w->rbuf){ log_msg("ws: rbuf alloc fail"); return -1; }
|
||||
if(net_ensure()<0) goto fail;
|
||||
int32_t rid = sceNetResolverCreate("orbisrpcR", 0, 0);
|
||||
OrbisNetInAddr in; memset(&in,0,sizeof in);
|
||||
int resolved = 0;
|
||||
@@ -79,39 +100,41 @@ int ws_connect(ws_t *w, const char *host, int port, const char *resource, const
|
||||
}
|
||||
if(!resolved){
|
||||
struct in_addr ia = { .s_addr = inet_addr(host) };
|
||||
if(ia.s_addr == 0xffffffff){ log_msg("resolve fail: %s", host); return -4; }
|
||||
if(ia.s_addr == 0xffffffff){ log_msg("resolve fail: %s", host); goto fail; }
|
||||
in.s_addr = ia.s_addr;
|
||||
}
|
||||
/* TCP socket */
|
||||
int32_t fd = sceNetSocket("orbisrpcWs", ORBIS_NET_AF_INET, ORBIS_NET_SOCK_STREAM, 0);
|
||||
if(fd < 0){ log_msg("socket fail %d",fd); return -5; }
|
||||
if(fd < 0){ log_msg("socket fail %d",fd); goto fail; }
|
||||
w->fd = fd; w->sock = fd;
|
||||
/* Pack port + IPv4 into sa_data in network byte order.
|
||||
* s_addr is a big-endian u32 value; on this LE target its FIRST octet
|
||||
* lives in the HIGH byte, so shift down from the top.
|
||||
* (v1 packed it low-byte-first -> reversed IPs -> connect() to nowhere.) */
|
||||
OrbisNetSockaddr sa; memset(&sa,0,sizeof sa);
|
||||
sa.len = (uint8_t)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);
|
||||
sa.sa_data[2] = (char)((in.s_addr >> 24) & 0xff);
|
||||
sa.sa_data[3] = (char)((in.s_addr >> 16) & 0xff);
|
||||
sa.sa_data[4] = (char)((in.s_addr >> 8) & 0xff);
|
||||
sa.sa_data[5] = (char)(in.s_addr & 0xff);
|
||||
if(sceNetConnect(fd, &sa, sizeof sa) < 0){
|
||||
log_msg("connect fail to %s:%d", host, port);
|
||||
sceNetSocketClose(fd); return -6;
|
||||
goto fail;
|
||||
}
|
||||
/* non-blocking so SSL_read never stalls the poll loop */
|
||||
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. 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. */
|
||||
w->nb = 1;
|
||||
/* socket already non-blocking -> SSL_connect returns WANT_*; poll it */
|
||||
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; }
|
||||
/* SNI: SSL_set_tlsext_host_name = SSL_ctrl(s, SSL_CTRL_SET_TLSEXT_HOSTNAME(55),
|
||||
* TLSEXT_NAMETYPE_host_name(0), hostname). Discord TLS needs the host. */
|
||||
w->ssl_ctx = (int32_t)(intptr_t)ctx; /* stash early: single cleanup path */
|
||||
w->ssl = (int32_t)(intptr_t)ssl;
|
||||
if(SSL_set_fd(ssl, (int)fd) != 1){ log_msg("SSL_set_fd fail"); goto fail; }
|
||||
/* SNI: SSL_ctrl(s, SSL_CTRL_SET_TLSEXT_HOSTNAME(55), NAMETYPE_host_name(0), host) */
|
||||
if(SSL_ctrl(ssl, 55, 0, (void *)host) != 1){ log_msg("SNI warn"); }
|
||||
SSL_set_connect_state(ssl);
|
||||
int cr=-1; int64_t t0=time(NULL);
|
||||
@@ -120,15 +143,13 @@ int ws_connect(ws_t *w, const char *host, int port, const char *resource, const
|
||||
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; }
|
||||
if(time(NULL)-t0 > 10){ 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. Desktop-client UA, and NO Origin header:
|
||||
* native clients (unlike browsers) don't send Origin to the gateway. */
|
||||
/* HTTP Upgrade handshake. Desktop-client UA, NO Origin header (native
|
||||
* clients don't send Origin to the gateway). */
|
||||
char req[640]; int n=snprintf(req,sizeof req,
|
||||
"GET %s HTTP/1.1\r\nHost: %s:%d\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
|
||||
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||
@@ -136,10 +157,10 @@ int ws_connect(ws_t *w, const char *host, int port, const char *resource, const
|
||||
"Safari/537.36\r\n"
|
||||
"Sec-WebSocket-Key: %s\r\nSec-WebSocket-Version: 13\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 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);
|
||||
if(ws_send_all(w, (const unsigned char*)req, (size_t)n) < 0){ log_msg("hs write fail"); goto fail; }
|
||||
/* Read response headers; bytes past "\r\n\r\n" are the first websocket
|
||||
* frame (usually HELLO arriving early) and MUST be kept, not dropped. */
|
||||
char hdr[2048]; 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){
|
||||
@@ -149,123 +170,152 @@ int ws_connect(ws_t *w, const char *host, int port, const char *resource, const
|
||||
}
|
||||
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; }
|
||||
if(time(NULL)-t0 > 10){ log_msg("hs timeout"); goto fail; }
|
||||
usleep(20000); continue;
|
||||
}
|
||||
goto fail; /* EOF or real error before 101 */
|
||||
goto fail;
|
||||
}
|
||||
if(hlen<12 || !strstr(hdr,"101")){ log_msg("no 101: %.40s", hdr); goto fail; }
|
||||
w->connected = 1;
|
||||
const char *body = strstr(hdr,"\r\n\r\n");
|
||||
if(body){
|
||||
body += 4;
|
||||
size_t bl = (size_t)(hdr+hlen-body);
|
||||
if(bl > w->rcap) bl = w->rcap;
|
||||
if(bl){ memcpy(w->rbuf, body, bl); w->rlen = bl; }
|
||||
}
|
||||
log_msg("ws: connected (handshake ok)");
|
||||
return 0;
|
||||
fail:
|
||||
if(w->ssl) SSL_free((SSL*)(intptr_t)w->ssl);
|
||||
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);
|
||||
if(w->fd > 0) sceNetSocketClose(w->fd);
|
||||
free(w->rbuf); w->rbuf=NULL; w->rcap=0;
|
||||
w->connected=0; w->ssl=0; w->ssl_ctx=0; w->fd=0; w->sock=0;
|
||||
return -9;
|
||||
}
|
||||
|
||||
/* --- 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;
|
||||
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; }
|
||||
/* RFC 6455 5.3: a fresh random mask per frame. Cheap LCG seeded per socket. */
|
||||
static uint32_t mk_seed;
|
||||
if(!mk_seed) mk_seed = (uint32_t)time(NULL) ^ (uint32_t)(uintptr_t)w ^ 0x9e3779b9u;
|
||||
mk_seed = mk_seed*1664525u + 1013904223u;
|
||||
unsigned char mk[4];
|
||||
mk[0]=(unsigned char)(mk_seed&0xff); mk[1]=(unsigned char)((mk_seed>>8)&0xff);
|
||||
mk[2]=(unsigned char)((mk_seed>>16)&0xff); mk[3]=(unsigned char)((mk_seed>>24)&0xff);
|
||||
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);
|
||||
if(len > 16384){ log_msg("ws frame too big (%zu); refusing", len); return -1; }
|
||||
unsigned char mk[4]; next_mask(mk);
|
||||
unsigned char hdr[14]; size_t f=0;
|
||||
hdr[f++]=0x81; /* FIN | text */
|
||||
if(len<126){ hdr[f++]=(unsigned char)(0x80|len); }
|
||||
else { hdr[f++]=0x80|126; hdr[f++]=(unsigned char)((len>>8)&0xff); hdr[f++]=(unsigned char)(len&0xff); }
|
||||
memcpy(hdr+f, mk, 4); f+=4;
|
||||
unsigned char *buf=(unsigned char*)malloc(f+len);
|
||||
if(!buf) return -1;
|
||||
memcpy(buf,hdr,f);
|
||||
for(size_t i=0;i<len;i++) buf[f+i]=(unsigned char)msg[i]^mk[i%4];
|
||||
int r = ws_send_all(w, buf, f+len);
|
||||
free(buf);
|
||||
return r<0?-1:r;
|
||||
}
|
||||
|
||||
int ws_send_ping(ws_t *w){
|
||||
/* RFC 6455: client control frames (close/ping/pong) are masked too. */
|
||||
static int ws_send_control(ws_t *w, unsigned char op){
|
||||
if(!w->connected) return -1;
|
||||
unsigned char p[2]={0x89,0x00};
|
||||
return SSL_write((SSL*)(intptr_t)w->ssl, p, 2);
|
||||
unsigned char mk[4]; next_mask(mk);
|
||||
unsigned char f[6] = { (unsigned char)(0x80|op), 0x80, mk[0], mk[1], mk[2], mk[3] };
|
||||
return ws_send_all(w, f, 6);
|
||||
}
|
||||
|
||||
/* 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),
|
||||
* advances rpos past the frame, and returns the number of bytes copied into buf
|
||||
* (may be less than the full payload length for oversized frames).
|
||||
* Returns 0 if an incomplete frame is pending. */
|
||||
static int try_parse_frame(ws_t *w, char *buf, size_t cap, int *opcode_out, int *fin_out){
|
||||
const unsigned char *b = w->rbuf + w->rpos;
|
||||
/* Peek at the pending frame header without consuming. 1 = full header ready. */
|
||||
static int peek_frame(ws_t *w, size_t *hdr_out, uint64_t *plen_out){
|
||||
size_t avail = w->rlen - w->rpos;
|
||||
const unsigned char *b = w->rbuf + w->rpos;
|
||||
if(avail < 2) return 0;
|
||||
int fin = (b[0]&0x80)!=0, op = b[0]&0x0f;
|
||||
size_t plen = b[1]&0x7f, hdr = 2;
|
||||
if(plen==126){
|
||||
if(avail < 4) return 0;
|
||||
plen = (b[2]<<8)|b[3]; hdr=4;
|
||||
plen = (size_t)((b[2]<<8)|b[3]); hdr=4;
|
||||
} else if(plen==127){
|
||||
if(avail < 10) return 0;
|
||||
plen=0; for(int i=2;i<10;i++) plen=(plen<<8)|b[i]; hdr=10;
|
||||
plen=0; for(int i=2;i<10;i++) plen=(plen<<8)|b[i];
|
||||
hdr=10;
|
||||
}
|
||||
if(avail < hdr+plen) return 0; /* not complete yet */
|
||||
const unsigned char *p = b+hdr;
|
||||
/* server frames are unmasked; handle mask bit defensively */
|
||||
size_t copy = plen; if(copy>cap-1) copy=cap-1;
|
||||
memcpy(buf, p, copy);
|
||||
buf[copy]=0;
|
||||
size_t consumed = hdr+plen;
|
||||
w->rpos += consumed;
|
||||
if(w->rpos >= w->rlen){ w->rpos = 0; w->rlen = 0; }
|
||||
if(opcode_out)*opcode_out=op; if(fin_out)*fin_out=fin;
|
||||
return (int)copy; /* only what actually landed in buf */
|
||||
*hdr_out=hdr; *plen_out=(uint64_t)plen;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ws_recv_frame(ws_t *w, char *buf, size_t cap, int *opcode_out, int *fin_out){
|
||||
if(!w->connected) return -1;
|
||||
SSL *ssl = (SSL*)(intptr_t)w->ssl;
|
||||
for(;;){
|
||||
/* compact the buffer when it's been fully consumed */
|
||||
if(w->rpos==0 && w->rlen==0) { /* empty */ }
|
||||
else if(w->rpos>0){
|
||||
/* compact consumed bytes to the front */
|
||||
if(w->rpos>0){
|
||||
memmove(w->rbuf, w->rbuf+w->rpos, w->rlen-w->rpos);
|
||||
w->rlen -= w->rpos; w->rpos = 0;
|
||||
}
|
||||
int got = try_parse_frame(w, buf, cap, opcode_out, fin_out);
|
||||
if(got != 0) return got;
|
||||
/* need more bytes */
|
||||
if(w->rlen >= sizeof w->rbuf){ /* buffer full, frame too big */
|
||||
w->rlen = 0; w->rpos = 0; return -2;
|
||||
if(w->skip_left>0){
|
||||
/* draining an oversized frame: drop whatever is buffered */
|
||||
size_t have = w->rlen - w->rpos;
|
||||
size_t take = have < (size_t)w->skip_left ? have : (size_t)w->skip_left;
|
||||
w->skip_left -= take; w->rpos += take;
|
||||
if(w->rpos >= w->rlen){ w->rpos=0; w->rlen=0; }
|
||||
if(w->skip_left==0){
|
||||
if(opcode_out)*opcode_out=w->skip_op;
|
||||
return -3; /* whole oversized frame skipped */
|
||||
}
|
||||
} else {
|
||||
size_t hdr; uint64_t plen;
|
||||
if(peek_frame(w,&hdr,&plen)){
|
||||
const unsigned char *b = w->rbuf + w->rpos;
|
||||
int fin=(b[0]&0x80)!=0, wire_op=b[0]&0x0f;
|
||||
/* absurd length: skip BEFORE arithmetic (hdr+plen could
|
||||
* overflow uint64 and smuggle a tiny "total" past the cap) */
|
||||
if(plen > WS_RBUF_MAX){
|
||||
size_t avail = w->rlen - w->rpos;
|
||||
size_t h = (hdr < avail) ? hdr : avail;
|
||||
w->rpos += h; /* eat the header */
|
||||
size_t payload_here = avail - h;
|
||||
w->skip_left = plen - (uint64_t)payload_here;
|
||||
w->skip_op = wire_op;
|
||||
if(w->rpos >= w->rlen){ w->rpos=0; w->rlen=0; }
|
||||
continue;
|
||||
}
|
||||
uint64_t total = (uint64_t)hdr + plen;
|
||||
if(total > (uint64_t)w->rcap){
|
||||
size_t ncap=w->rcap;
|
||||
while(ncap < (size_t)total && ncap < WS_RBUF_MAX) ncap*=2;
|
||||
unsigned char *nb=(unsigned char*)realloc(w->rbuf,ncap);
|
||||
if(nb){ w->rbuf=nb; w->rcap=ncap; log_msg("ws: rbuf grown to %zu", ncap); }
|
||||
else { log_msg("ws: rbuf grow fail"); }
|
||||
}
|
||||
if((uint64_t)(w->rlen-w->rpos) >= total){
|
||||
size_t copy = (size_t)plen;
|
||||
if(copy > cap-1) copy = cap-1;
|
||||
memcpy(buf, b+hdr, copy);
|
||||
buf[copy]=0;
|
||||
w->rpos += (size_t)total;
|
||||
if(w->rpos >= w->rlen){ w->rpos=0; w->rlen=0; }
|
||||
if(opcode_out)*opcode_out=wire_op;
|
||||
if(fin_out)*fin_out=fin;
|
||||
return (int)copy;
|
||||
}
|
||||
}
|
||||
if(w->rlen == w->rcap){ /* full with no parseable frame: give up cleanly */
|
||||
w->rpos=0; w->rlen=0; return -2;
|
||||
}
|
||||
}
|
||||
int rd = SSL_read((SSL*)(intptr_t)w->ssl, w->rbuf+w->rlen, (int)(sizeof w->rbuf-w->rlen));
|
||||
if(rd > 0){ w->rlen += rd; continue; }
|
||||
if(rd == 0){
|
||||
int e = SSL_get_error((SSL*)(intptr_t)w->ssl, rd);
|
||||
if(e==SSL_ERROR_WANT_READ || e==SSL_ERROR_WANT_WRITE) return 0; /* no data yet */
|
||||
return -1; /* connection closed */
|
||||
}
|
||||
return -1;
|
||||
int rd = SSL_read(ssl, w->rbuf+w->rlen, (int)(w->rcap-w->rlen));
|
||||
if(rd > 0){ w->rlen += (size_t)rd; continue; }
|
||||
int e = SSL_get_error(ssl, rd);
|
||||
if(e==SSL_ERROR_WANT_READ || e==SSL_ERROR_WANT_WRITE) return 0; /* no data yet */
|
||||
return -1; /* closed / error */
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if(!w->connected){ free(w->rbuf); w->rbuf=NULL; w->rcap=0; return 0; }
|
||||
ws_send_control(w, 0x8); /* best-effort masked CLOSE */
|
||||
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);
|
||||
free(w->rbuf); w->rbuf=NULL; w->rcap=0;
|
||||
w->connected=0; w->sock=0; w->ssl=0; w->ssl_ctx=0; w->fd=0; w->rlen=0; w->rpos=0;
|
||||
w->skip_left=0;
|
||||
return 0;
|
||||
}
|
||||
+11
-9
@@ -6,23 +6,25 @@
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#define WS_RBUF 16384 /* raw socket buffer (large enough for gateway dispatches) */
|
||||
#define WS_RBUF_MIN 16384 /* initial raw socket buffer */
|
||||
#define WS_RBUF_MAX (2*1024*1024) /* grow limit; bigger frames are skipped */
|
||||
|
||||
typedef struct {
|
||||
int32_t sock; int32_t ssl; int32_t ssl_ctx;
|
||||
int32_t connected; int32_t fd;
|
||||
int nb; /* underlying socket non-blocking flag */
|
||||
unsigned char rbuf[WS_RBUF]; /* raw bytes from SSL_read */
|
||||
size_t rlen; /* valid bytes in rbuf */
|
||||
size_t rpos; /* consumed parse position */
|
||||
int nb; /* underlying socket non-blocking flag */
|
||||
unsigned char *rbuf; /* raw bytes from SSL_read (heap, grows) */
|
||||
size_t rcap; /* allocated size of rbuf */
|
||||
size_t rlen; /* valid bytes in rbuf */
|
||||
size_t rpos; /* consumed parse position */
|
||||
uint64_t skip_left; /* bytes left of an oversized frame being drained */
|
||||
int skip_op; /* opcode of the frame being drained */
|
||||
} 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_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). */
|
||||
/* Returns frame payload length (>0), 0 if no complete frame yet, <0 on error,
|
||||
* or -3 if an oversized frame was drained and skipped (payload lost). */
|
||||
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
|
||||
+2
-2
@@ -20,13 +20,13 @@ INTDIR := $(BUILD_DIR)
|
||||
|
||||
# Our daemon sources (payload main.c is NOT part of the plugin; plugin.c is the entry)
|
||||
DAEMON_SRC := $(ORBISRPC)/log.c $(ORBISRPC)/cfg.c $(ORBISRPC)/jsonlite.c $(ORBISRPC)/b64.c \
|
||||
$(ORBISRPC)/http.c $(ORBISRPC)/ws.c $(ORBISRPC)/discord.c $(ORBISRPC)/detect.c \
|
||||
$(ORBISRPC)/ws.c $(ORBISRPC)/discord.c $(ORBISRPC)/detect.c \
|
||||
$(ORBISRPC)/daemon.c
|
||||
PLUGIN_SRC := $(SRCDIR)/plugin.c
|
||||
CFILES := $(PLUGIN_SRC) $(DAEMON_SRC)
|
||||
OBJS := $(patsubst %.c,$(INTDIR)/%.o,$(notdir $(CFILES)))
|
||||
|
||||
LIBS := -lSceLibcInternal -lSceNet -lSceNetCtl -lSceLibreSSL -lSceSsl -lSceHttp \
|
||||
LIBS := -lSceLibcInternal -lSceNet -lSceNetCtl -lSceLibreSSL \
|
||||
-lSceSysmodule -lSceUserService -lSceAppInstUtil -lSceAppContent \
|
||||
-lGoldHEN_Hook -lkernel
|
||||
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ 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 -lSceLibreSSL -lSceSsl -lSceHttp -lSceSysmodule \
|
||||
LIBS="-lc -lkernel -lSceNet -lSceNetCtl -lSceLibreSSL -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)"
|
||||
@@ -19,7 +19,7 @@ cd "$ROOT"
|
||||
export OO_PS4_TOOLCHAIN="$SDK"
|
||||
OUT="$ROOT/build"; mkdir -p "$OUT"
|
||||
echo "=== compiling ==="
|
||||
for f in log cfg jsonlite b64 http ws detect discord daemon main; do
|
||||
for f in log cfg jsonlite b64 ws detect discord daemon main; do
|
||||
"$CC" $CFLAGS -c -o "$OUT/$f.o" "orbisrpc/$f.c" || { echo "compile $f FAILED"; exit 1; }
|
||||
done
|
||||
echo "=== linking ($LD) ==="
|
||||
|
||||
Reference in new issue
Block a user