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:
SirHumza committed 2026-08-23 11:07:38 +02:00
1 parent 91382fc53c
commit 949240f590
14 files changed
+457 -606

No files matched your search

+27 -28
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
-14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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) ==="