mirror of
https://github.com/SirHumza/orbisRPC.git
synced 2026-10-06 09:00:37 +02:00
orbisRPC — history-free import of main
This commit is contained in:
commit
3e0f4b492c
694 files changed
+617372
No files matched your search
@@ -0,0 +1,38 @@
|
||||
name: ci
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
host:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: host unit tests
|
||||
run: make -C tests clean && make -C tests test
|
||||
- name: ASan/UBSan
|
||||
run: make -C tests asan
|
||||
- name: e2e contracts
|
||||
run: python3 tests/e2e_consumer.py
|
||||
sdk-payload:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: install llvm
|
||||
run: sudo apt-get update -qq && sudo apt-get install -qy llvm lld
|
||||
- name: fetch ps4-payload-sdk
|
||||
run: |
|
||||
curl -sL -o /tmp/sdk.zip https://github.com/ps4-payload-dev/sdk/releases/latest/download/ps4-payload-sdk.zip
|
||||
mkdir -p ~/ps4-payload-sdk && unzip -q -o /tmp/sdk.zip -d ~/ps4-payload-sdk
|
||||
- name: build SDK daemon payload
|
||||
run: |
|
||||
export PS4_PAYLOAD_SDK="$HOME/ps4-payload-sdk/ps4-payload-sdk"
|
||||
export PATH="/usr/lib/llvm-14/bin:$PATH"
|
||||
./scripts/build_sdk.sh
|
||||
- name: gate daemon-world linkage only
|
||||
run: |
|
||||
export LLVM_READELF="$(command -v llvm-readelf || echo /usr/lib/llvm-14/bin/llvm-readelf)"
|
||||
NEEDED=$("$LLVM_READELF" -d build-sdk/orbisrpc_sdk.elf | grep NEEDED | sed 's/.*\[//;s/\]//')
|
||||
echo "$NEEDED"
|
||||
echo "$NEEDED" | grep -q libkernel_web.sprx || { echo "missing libkernel_web"; exit 1; }
|
||||
echo "$NEEDED" | grep -q libSceLibcInternal.sprx || { echo "missing libSceLibcInternal"; exit 1; }
|
||||
echo "$NEEDED" | grep -q libSceNet.sprx || { echo "missing libSceNet"; exit 1; }
|
||||
if echo "$NEEDED" | grep -q 'libkernel\.so'; then echo "FORBIDDEN: app-world libkernel.so"; exit 1; fi
|
||||
echo "linkage OK"
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
build/
|
||||
*.o
|
||||
*.err
|
||||
*.elf
|
||||
*.fself
|
||||
*.prx
|
||||
*.oelf
|
||||
/tmp/
|
||||
.DS_Store
|
||||
config/config.ps4.json
|
||||
plugin/build/
|
||||
tests/test_utils
|
||||
config/icons/*
|
||||
!config/icons/logo.png
|
||||
|
||||
# installer artifacts
|
||||
*.pkg
|
||||
installer/x64/
|
||||
installer/eboot.bin
|
||||
installer/pkg.gp4
|
||||
installer/sce_sys/param.sfo
|
||||
build-sdk/
|
||||
|
||||
# installer build outputs
|
||||
installer/x64/
|
||||
installer/eboot.bin
|
||||
installer/pkg.gp4
|
||||
installer/assets/daemon.elf
|
||||
installer/assets/evict.elf
|
||||
installer/sce_sys/param.sfo
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
# HANDOFF — OrbisRPC PS4 Rich Presence
|
||||
|
||||
Date: 2026-09-24. Branch: `main` (pushed). Console: PS4 9.00, GoldHEN 2.4,
|
||||
`192.168.1.136` (FTP :2121 anon, klog :3232, BinLoader one-shot :9020).
|
||||
|
||||
## Folder layout (repo root)
|
||||
|
||||
- `orbisrpc/` — all daemon sources. Key files: `daemon.c` (loop/sessions),
|
||||
`discord.c` (presence serialize), `detect.c` (game finding), `tmdb.c` +
|
||||
`art_table.h` (Sony metadata), `art.c` (mp: proxy resolve), `tls.c` +
|
||||
`ws.c` (transport), `updater.c` + `manifest.c` + `health.c` (updates),
|
||||
`timesync.c` (SNTP), `lock.c`, `cfg.c`, `log.c`, `jsonlite.c`.
|
||||
- `scripts/build_sdk.sh` — builds the daemon payload with ps4-payload-sdk
|
||||
(`build-sdk/orbisrpc_sdk.elf`). Toolchain: `~/ps4-payload-sdk`, shim in
|
||||
`~/llvmshim`. NEVER link `libkernel.so` (instant death in spawn context;
|
||||
gate in CI + `tests/e2e_consumer.py`).
|
||||
- `tests/` — host unit tests + e2e contracts. `make test`, `make asan`.
|
||||
- `scripts/build_art_table.py` + `titles.txt` — regenerates art table/icons.
|
||||
- `docs/injecting.md` — how payloads get onto the console (one-shot rule!).
|
||||
- `findings/` — hardware research archive (loaders, TLS PSA, IME, linkage).
|
||||
- `scripts/ps4_watch.py` — one-shot console state diff (repo copy may be
|
||||
absent; logic: FTP file sizes + ports + newest screenshot).
|
||||
|
||||
## Existing issues (ranked, all reproduced)
|
||||
|
||||
1. **Cover art tile** — shows "?" instead of game art. mp: proxy renders
|
||||
(proven live), uploaded asset stuck `private`, external URLs drop the
|
||||
whole activity. Daemon resolves mp: per session (`art: resolved mp` in
|
||||
log). Needs: propagation wait or per-title asset strategy (below).
|
||||
2. **Stale title on fresh launches** — save-mtimes get bulk-touched by
|
||||
cloud sync (all 28 titles shared one mtime!); app.pkg atime added as
|
||||
sharper signal; eboot-set change fast-path added. Unproven live.
|
||||
3. **Game crashes (CE-34878-0)** on some titles with daemon on — no
|
||||
mechanism found after full audit; console has prior crash history.
|
||||
Needs clean-boot test (reboot, no payload, launch).
|
||||
4. **IME keyboard** never opens (0x80BC0010 through six fixes). Token is
|
||||
pre-seeded via FTP; do not block on this.
|
||||
5. **Rest Mode** — wake/resume coded, never tested through a real cycle.
|
||||
6. **Updater drill** — staged-update path never tested live.
|
||||
7. **Plugin path** — PRX can't deploy via FTP (console converts SELF→ELF);
|
||||
must ship inside PKG. Untested live.
|
||||
8. **`main` vs `WIP` branches** — main = proven runtime line; WIP = old
|
||||
1.0 product line (installer/app/docs). Do not merge blindly.
|
||||
|
||||
## What could be better (next agent: pick these up)
|
||||
|
||||
- **Rest Mode field test** + home-screen presence proof.
|
||||
- **Updater live drill** (stage → reboot → rollback check).
|
||||
- **IME**: needs on-console iteration with return-code logging (already
|
||||
instrumented in `pkg/app_main.c` on the WIP line).
|
||||
- **Plugin live test** via PKG delivery.
|
||||
- **e2e/CI**: `.github/workflows/ci.yml` exists; extend coverage for new
|
||||
modules (lock, timesync, art, session persistence).
|
||||
- **Playtime ledger reader** (totals per title; data already banked).
|
||||
- **Media-type table**: only Netflix/YouTube mapped; add IDs as found.
|
||||
|
||||
## Auto icons from Sony CDN (no manual adding) — the design
|
||||
|
||||
Goal: never hand-add a title again. Three layers, in order:
|
||||
|
||||
1. **Live TMDB-over-TLS** (`tmdb_https_get()` in `tmdb.c`, already coded):
|
||||
queries `tmdb.np.dl.playstation.net:443` for any title ID at resolve
|
||||
time. Port 80 is blocked on-console; **443 was never proven — first
|
||||
job is running `probe_tmdb443.elf` on hardware.** If green, every
|
||||
current and future game resolves fully automatically.
|
||||
2. **Self-growing table fallback**: daemon already logs unknown title IDs
|
||||
implicitly (raw-ID sessions). Add a CI job: parse `titles.txt` +
|
||||
any new IDs seen, run `build_art_table.py`, commit regenerated
|
||||
`art_table.h` + icons. Table then grows itself with zero hand edits.
|
||||
3. **mp: proxy at post time** (`art.c`, proven rendering): whatever URL
|
||||
layers 1–2 produce gets proxied per session; cache expires per game
|
||||
switch so mappings never go stale.
|
||||
|
||||
Rule: hands touch art ZERO times — new game installs resolve through
|
||||
layer 1, or land in the table via layer 2's automation.
|
||||
@@ -0,0 +1,80 @@
|
||||
<p align="center">
|
||||
<img src="config/icons/logo.png" width="420" alt="orbisRPC">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/SirHumza/orbisRPC/releases/tag/v1.0.0"><img src="https://img.shields.io/badge/version-1.0.0-ffd800?style=flat-square" alt="version"></a>
|
||||
<img src="https://img.shields.io/badge/PS4-GoldHEN-003791?style=flat-square" alt="PS4 GoldHEN">
|
||||
<img src="https://img.shields.io/badge/Discord-Rich%20Presence-5865F2?style=flat-square" alt="Discord">
|
||||
<img src="https://img.shields.io/badge/tables-none-brightgreen?style=flat-square" alt="no tables">
|
||||
</p>
|
||||
|
||||
<p align="center"><b>Discord Rich Presence for the jailbroken PS4 — every game, zero setup.</b><br>
|
||||
A background daemon that lives entirely on your console and posts what you're
|
||||
playing to Discord: name, cover art, timer. No PC at runtime.</p>
|
||||
|
||||
---
|
||||
|
||||
## Install (5 minutes)
|
||||
|
||||
1. Grab `OrbisRPC-Setup-1.0.0.pkg` from the
|
||||
[Releases page](https://github.com/SirHumza/orbisRPC/releases/tag/v1.0.0)
|
||||
and install it with Package Installer.
|
||||
2. Open **orbisRPC Setup** → say Yes. It drops the daemon into GoldHEN's
|
||||
`bin/elf` and saves your Discord token.
|
||||
3. Start **orbisrpc** from GoldHEN's payload menu, launch a game, watch Discord.
|
||||
|
||||
After a reboot: re-jailbreak, then enable AutoRun for `orbisrpc` in GoldHEN's
|
||||
payload menu once — it starts itself on every jailbreak after that.
|
||||
|
||||
## What you get
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 🎮 **Any game, no lists** | Names resolve from your console's own database — CUSA, PPSA, indies, all covered with zero per-game setup. |
|
||||
| 🖼️ **Real cover art** | Game art served per title, PlayStation logo when idle. |
|
||||
| ⏱️ **True timers** | Survive reconnects and restarts, resume across quick game switches. |
|
||||
| 🧠 **Self-learning** | First-seen titles are remembered, so later boots resolve instantly. |
|
||||
| 🔄 **Self-updating** | Signed daemon updates with boot rollback. No reinstall treadmill. |
|
||||
| 📦 **One-tap installer** | Setup PKG: install → token → payload in place. |
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
PS4 (GoldHEN) Discord
|
||||
┌─────────────────────────┐ ┌──────────────────┐
|
||||
│ orbisRPC daemon │ TLS │ your profile │
|
||||
│ sandbox scan → game ID │ ◄──► │ Playing Game │
|
||||
│ app.db → display name │ │ [cover] [timer] │
|
||||
└─────────────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
Details: [`docs/DAEMON.md`](docs/DAEMON.md) · [`docs/INSTALLER.md`](docs/INSTALLER.md) ·
|
||||
[`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) ·
|
||||
[`docs/PRODUCTION.md`](docs/PRODUCTION.md)
|
||||
|
||||
## Config
|
||||
|
||||
Only `token` (your Discord user session token) is required —
|
||||
`/data/orbisRPC/config.json`. Everything else ships working: presence text,
|
||||
home art, poll interval, plus the self-learned `titles` map (hands off).
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
./scripts/build_sdk.sh # daemon payload (needs ps4-payload-sdk)
|
||||
make -f installer/Makefile # Setup PKG (OpenOrbis toolchain, llvmshim)
|
||||
make -C tests test # host unit tests
|
||||
make -C tests asan # ASan/UBSan
|
||||
python3 tests/e2e_consumer.py # contracts + linkage gate
|
||||
```
|
||||
|
||||
## Safety
|
||||
|
||||
A user session token grants full account access — never share the config,
|
||||
never commit a real token. Token-based presence is against Discord's ToS
|
||||
(standard practice for headless presence tools; risk is yours).
|
||||
|
||||
## License
|
||||
|
||||
To be finalized (MIT vs GPL). No GPL source is copied into this tree.
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"token": "SET_ME",
|
||||
"application_id": "1536977374795538532",
|
||||
"art_base_url": "https://raw.githubusercontent.com/SirHumza/orbisrpc-host/main/icons/",
|
||||
"enabled": 1,
|
||||
"poll_interval_s": 12,
|
||||
"presence_state": "On PS4"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 381 KiB |
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"CUSA00001": "THE PLAYROOM",
|
||||
"CUSA00219": "Destiny",
|
||||
"CUSA00568": "Destiny",
|
||||
"CUSA00572": "SHAREfactory™",
|
||||
"CUSA00960": "PlayStation™ Vue",
|
||||
"CUSA01000": "Destiny",
|
||||
"CUSA01697": "PlayStation™Now",
|
||||
"CUSA01780": "Spotify",
|
||||
"CUSA02012": "Media Player",
|
||||
"CUSA06014": "TEKKEN™7",
|
||||
"FLTZ00003": "Remote PKG installer",
|
||||
"LAPY20009": "PS4-Xplorer 2.0",
|
||||
"NPXS20102": "Internet Browser",
|
||||
"NPXS20104": "Capture Gallery",
|
||||
"NPXS20105": "Live from PlayStation",
|
||||
"NPXS20106": "Music Unlimited",
|
||||
"NPXS20107": "PlayStation™Video - My Videos",
|
||||
"NPXS20108": "What's New",
|
||||
"NPXS20109": "",
|
||||
"NPXS20110": "TV & Video",
|
||||
"NPXS20111": "Library",
|
||||
"NPXS20112": "",
|
||||
"NPXS20114": "PlayStation™Now",
|
||||
"NPXS20117": "USB Music Player",
|
||||
"NPXS20118": "Share Play",
|
||||
"NPXS20120": "PlayStation™Video",
|
||||
"NPXS20132": "",
|
||||
"NPXS20133": "",
|
||||
"NPXS20979": "PlayStation Store",
|
||||
"NPXS21021": "PS5 Remote Play",
|
||||
"NPXS39041": "Store",
|
||||
"CUSA13323": "Ghost of Tsushima",
|
||||
"CUSA11995": "Marvel's Spider-Man",
|
||||
"CUSA20177": "Marvel's Spider-Man: Miles Morales",
|
||||
"LAPY20006": "PS4 Temperature",
|
||||
"CUSA01015": "YouTube",
|
||||
"CUSA00411": "Grand Theft Auto V",
|
||||
"CUSA26618": "Grand Theft Auto: San Andreas – The Definitive Edition",
|
||||
"CHTM00777": "PS4 Cheats Manager",
|
||||
"PKGI13337": "FPKGi",
|
||||
"MANU90003": "FPKGi Data",
|
||||
"CUSA00135": "BATMAN™: ARKHAM KNIGHT",
|
||||
"CUSA08992": "DRAGON BALL FighterZ",
|
||||
"CUSA03245": "Deadpool",
|
||||
"CUSA06712": "Goat Simulator",
|
||||
"CUSA01839": "STREET FIGHTER IV",
|
||||
"CUSA14909": "SpongeBob SquarePants: Battle For Bikini Bottom",
|
||||
"CUSA24899": "Stray",
|
||||
"CUSA00740": "Terraria",
|
||||
"CUSA20499": "Cuphead",
|
||||
"CUSA00021": "WATCH_DOGS™",
|
||||
"CUSA18795": "Skater XL",
|
||||
"APOL00004": "Apollo Save Tool",
|
||||
"CUSA04602": "Batman: Arkham City",
|
||||
"CUSA01004": "Sleeping Dogs",
|
||||
"CUSA04294": "WATCH_DOGS® 2",
|
||||
"LAPY20015": "Avatar Changer",
|
||||
"GBTX00001": "GameBaTo",
|
||||
"CUSA00127": "Netflix"
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
# Cover art pipeline
|
||||
|
||||
## Path A: Sony TMDB icon URLs (primary, fully automatic)
|
||||
|
||||
Every title lookup also queries Sony's TMDB service, which returns an
|
||||
official 512x512 icon URL on Sony's CDN. The daemon sends it as
|
||||
`assets.large_image` (Discord accepts external image URLs there). No
|
||||
uploads, no hosting, no per-game work, works on any console. If the
|
||||
service lacks the title or the network fails, the next path is tried.
|
||||
|
||||
## Path B: icon pack + external URLs (fallback, maintainer-hosted)
|
||||
|
||||
## Path A: icon pack + external URLs (recommended)
|
||||
|
||||
The daemon sends `assets.large_image` as
|
||||
`<art_base_url><lowercase titleId>.png` (Discord accepts external image
|
||||
URLs in asset fields). Host the pack once, e.g. in this repo:
|
||||
|
||||
```
|
||||
config/icons/cusa00740.png (512x512 PNG, pulled from the console)
|
||||
```
|
||||
|
||||
`scripts/sync_icons.sh` pulls every `/user/appmeta/<TITLEID>/icon0.png`
|
||||
off the PS4 over FTP (read-only) into `config/icons/`. Set
|
||||
`art_base_url` in config to the hosted prefix, e.g.
|
||||
`https://raw.githubusercontent.com/<you>/orbisRPC/main/config/icons/`.
|
||||
Every install then shows art with zero setup. Missing files degrade to
|
||||
no image. Pack hosting is a maintainer decision (game art is not ours).
|
||||
|
||||
## Path B: shared Discord application (fallback)
|
||||
|
||||
Create an application, upload one PNG per game under Art Assets named
|
||||
as the lowercase title ID, set `application_id` in config. The daemon
|
||||
sends the asset key instead. Capped at 300 assets per app.
|
||||
|
||||
Current default: the public PS4-Rich-Presence-for-Discord application
|
||||
(zorua98741/bshar1865, ID 858345055966461973), which already hosts
|
||||
per-title art. Borrowed backend, credited here: if it ever goes away,
|
||||
art degrades to nothing and everything else keeps working. Replace the
|
||||
default with your own app ID to own the whole chain.
|
||||
|
||||
## Runtime behavior
|
||||
|
||||
URL pack wins when `art_base_url` is set, uploaded keys when only
|
||||
`application_id` is set, no artwork otherwise (one log notice). Missing
|
||||
art never crashes anything.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Daemon design (v1.0.0)
|
||||
|
||||
## Loop
|
||||
|
||||
Outer connect cycles (jittered exponential backoff, 60 s cap ×10, then
|
||||
10-minute cadence, never exits) wrap an inner 1 s session loop. Token-4004
|
||||
never kills the daemon at either level: it backs off and retries so a fixed
|
||||
token lands on its own.
|
||||
|
||||
## States
|
||||
|
||||
`NONE -> HOME <-> GAME`, every transition logged. Game commits need 2
|
||||
consecutive polls; closes need 2 misses. Home posts once (timerless logo
|
||||
tile) and re-posts after every reconnect (a drop can never leave a blank
|
||||
tile). Every 15 min the current state reposts regardless (reconciliation).
|
||||
|
||||
## Detection
|
||||
|
||||
1. `/mnt/sandbox/<ID>_000` mount = authoritative running ID.
|
||||
2. eboot-process count change = launch/close right now (fast-switch).
|
||||
3. Save/appdir/app.pkg-atime signals disambiguate.
|
||||
|
||||
## Names (first hit wins, no baked tables ever)
|
||||
|
||||
1. `titles` map in config (manual override + self-learned).
|
||||
2. System app.db, read-only SQLite: `tbl_appbrowse.titleName`, then
|
||||
`tbl_appinfo` TITLE keys, then `/user/appmeta/<id>/param.json`.
|
||||
3. Local pronunciation.xml / param.sfo / app.xml.
|
||||
4. Sony TMDB live (`<ID>_00` + HMAC-SHA1 URL — byte-identical to the PC
|
||||
tools; unreachable from most consoles, kept as fallback).
|
||||
5. Raw ID (never shown twice; retried after 5 min, never frozen).
|
||||
|
||||
## Art
|
||||
|
||||
mp: proxy via external-assets → uploaded asset key → icon-pack pattern.
|
||||
Dangling keys/URLs are dropped, never sent (they blank the activity).
|
||||
Game tile carries a small system badge. 7-day disk cache + memory cache.
|
||||
|
||||
## Time, sessions, health
|
||||
|
||||
SNTP (connected UDP, Nov 2023–Dec 2034 window) drives timer ms. Sessions
|
||||
persist atomically with validation; 10-min resume window; playtime ledger.
|
||||
Health: dirty-boot marker (marker-first ordering), crash counter, 3-strike
|
||||
safe mode, signed all-or-nothing updates with boot rollback.
|
||||
Single-instance lock with sysctl liveness + exact-name check.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Installer (Setup PKG, `ORPC00001`)
|
||||
|
||||
## What it does
|
||||
|
||||
One linear flow, forward-only (every No skips ahead, nothing loops back):
|
||||
confirm → evict old daemon (via `sceKernelLoadStartModule`) → copy
|
||||
`evict.elf` + `orbisrpc.bin` to `/data/payloads/` (mkdir -p +
|
||||
byte-count + FNV hash read-back) → pre-saved config with token
|
||||
(skips entry when valid) → done.
|
||||
Read-only status screen available via decline.
|
||||
|
||||
There is no WiFi check. The installer runs sandboxed, so its socket probe
|
||||
measures the app's network stack, not the payload's — and a FAILED line
|
||||
reads like a verdict on Discord itself. The daemon reports real reachability
|
||||
in its own log once started.
|
||||
|
||||
The installer never boots anything directly. Starting the daemon is
|
||||
Payload Guest's `/data/payloads/` directory — pick `evict.elf` first
|
||||
(removes old orbisrpc instance), then pick `orbisrpc.bin`.
|
||||
No loopback ports, no injection, no boot proof to go wrong.
|
||||
|
||||
## Install flow
|
||||
|
||||
```
|
||||
confirm → sceKernelLoadStartModule(evict.elf) — kills old daemon
|
||||
→ copy evict.elf to /data/payloads/evict.elf
|
||||
→ copy orbisrpc.bin to /data/payloads/orbisrpc.bin
|
||||
→ save config.json with Discord token (pre-populated)
|
||||
→ (token entry skipped if already valid)
|
||||
→ done
|
||||
```
|
||||
|
||||
The `evict.elf` is launched via `sceKernelLoadStartModule` during
|
||||
install — it reads `daemon.lock`, kills the running daemon, exits.
|
||||
`evict.elf` stays in `/data/payloads/` for future manual use.
|
||||
|
||||
## Navigation law
|
||||
|
||||
No branch ever returns to start. Cancel/skip always moves forward. The only
|
||||
exits are Done, explicit close, or a payload write failure.
|
||||
|
||||
Buttons: X = enter, O = back. The confirm dialog uses `YESNO` focused on
|
||||
Yes — the OpenOrbis default `YESNO_FOCUS_NO` puts the console's confirm
|
||||
button on No, which inverts the whole wizard.
|
||||
|
||||
## Console facts encoded
|
||||
|
||||
- Splash is hidden first thing in `main()` so module loading in
|
||||
`ui_init()` happens with the splash already gone — no visible delay
|
||||
between splash hide and first dialog.
|
||||
- Dialog/IME/IME-backend sysmodules + internal SYSTEM/USER/COMMON/PAD
|
||||
modules load before use (order matters — `sceCommonDialogInitialize()`
|
||||
precedes external module loads; PAD is unloaded on exit).
|
||||
- `stat()`/`fstat()` lie about sizes inside the sandbox (observed 4096/4160
|
||||
for a 2071552-byte file), so install proof is byte count + FNV hash on
|
||||
read-back, and status uses open-existence, never stat.
|
||||
- Config writes are atomic (tmp+fsync+rename) with read-back proof.
|
||||
- IME wait is bounded; asset key charset validated (bad keys blank the
|
||||
activity).
|
||||
- `evict.elf` is loaded via `sceKernelLoadStartModule` because `kill()`
|
||||
is blocked by the sandbox (EPERM).
|
||||
|
||||
## Auto-start
|
||||
|
||||
Payload Guest AutoRun: enable it for `orbisrpc` once and the daemon
|
||||
starts on every jailbreak. The queue is menu-managed; the installer
|
||||
shows where, it can't write the queue itself.
|
||||
|
||||
## Building
|
||||
|
||||
`make -f installer/Makefile` (`OO_PS4_TOOLCHAIN`, llvmshim). Staged assets:
|
||||
`daemon.elf`, `evict.elf`, `config.json`. The PKG ships all three —
|
||||
the installer copies both payloads, launches evict.elf to kill the
|
||||
old daemon, and pre-saves the config token.
|
||||
Output: `IV0000-ORPC00001_00-ORBISRPCSETUP000.pkg`.
|
||||
@@ -0,0 +1,80 @@
|
||||
# OrbisRPC Production Report
|
||||
|
||||
Build under review: `9d9c5b7` (daemon 0.4.0 line, `main`), payload
|
||||
`build-sdk/orbisrpc_sdk.elf` (2,071,536 B, daemon-world linkage only,
|
||||
zero baked titles). Verified: host unit + ASan/UBSan + e2e ALL PASS;
|
||||
linkage gate (libkernel_web / libSceLibcInternal / libSceNet, never
|
||||
libkernel.so). Running on-console at 192.168.1.136 via elfldr:9021.
|
||||
|
||||
## Architecture
|
||||
|
||||
- `daemon.c` — outer connect cycles (jittered backoff, 10-fail 10-min
|
||||
cadence) + inner session loop (12 s poll, 2-hit commit debounce,
|
||||
playback queue of one). Re-reads config every cycle; learned titles
|
||||
persist via atomic save. Token-4004 waits for a config fix, never exits.
|
||||
- `detect.c` — sandbox-mount scan (authoritative ID) + save/appdir/atime
|
||||
signals + eboot-count fast-switch. Name chain: config override ->
|
||||
app.db (SQLite readonly) -> appmeta/sfo/appxml -> Sony TMDB live ->
|
||||
raw-ID fallback. Tri-state unknown-hold, 5-min raw retry.
|
||||
- `discord.c`/`ws.c`/`tls.c` — gateway v10, TLS 1.2 ceiling,
|
||||
VERIFY_REQUIRED against curated bundle, masked frames, HB ack tracking.
|
||||
- `art.c` — mp: proxy via external-assets, pack-pattern fallback,
|
||||
uploaded-key fallback, memory + 7-day disk cache. Home tile: PlayStation
|
||||
logo default; game tile: small system badge.
|
||||
- `health.c`/`updater.c`/`manifest.c` — dirty-boot marker, 60 s stable
|
||||
gate, safe mode, signed all-or-nothing staging, boot rollback.
|
||||
- `cfg.c`/`lock.c`/`timesync.c` — schema'd fallible config, sysctl-liveness
|
||||
single instance, SNTP wall clock for timer ms.
|
||||
- `tools/evict.c` — SIGTERM-then-SIGKILL deploy rotation for the locked daemon.
|
||||
|
||||
## State machine
|
||||
|
||||
NONE -> HOME ("PlayStation 4" + logo) <-> GAME (name + art + badge +
|
||||
ticking timer). Media titles map to Watching/Listening. Resume window:
|
||||
same title back within 10 min (or cross-restart via session.json) resumes
|
||||
the timer. Sessions append to playtime.log ledger.
|
||||
|
||||
## Third-party logic ported (verified against source)
|
||||
|
||||
- PS4-Rich-Presence (Python/C#): TMDB `_00` HMAC construction (live-200
|
||||
verified), persistent mapped cache (self-learning map), title-ID hover
|
||||
text, sandbox/NPXS-skip detect, backoff cadence, asset-key fallback.
|
||||
- PSN-API tool: small system badge pattern.
|
||||
- SonicLoader: read-only app.db tiers (appbrowse/appinfo/param.json).
|
||||
- Deliberate skips: PSN credentials, UI editors, PS1/PS2 lists, extra
|
||||
toggles, third-party scrapes (reasons in git history/discussion).
|
||||
|
||||
## Fixed this cycle
|
||||
|
||||
Sandbox param.sfo source, details-ID duplication, user `titles`
|
||||
overrides, `home_art`, shipped logo default, app.db SQLite names,
|
||||
self-learning map, presence-builder test seam, small badge, strncpy NUL
|
||||
hardening, home_art validation, 4004 survival, cfg_save return,
|
||||
evict.elf deploy tool, CI ASan+e2e jobs.
|
||||
|
||||
## Not fixed (external / out of scope)
|
||||
|
||||
- Sony TMDB unreachable from the console (TCP filtered; correct URL
|
||||
proven from LAN). Mitigated: app.db + learning map.
|
||||
- GoldHEN settings-page loader segfaults every ELF (external).
|
||||
- IME dialog init errors (external API behavior).
|
||||
- Vesktop `?` tile (client quirk; phone renders fine).
|
||||
- Dead Discord notify webhook (cosmetic post-push hook).
|
||||
|
||||
## Risks
|
||||
|
||||
- PS4 app.db schema varies by firmware: tiers fail soft, fallback chain
|
||||
covers. Worst case is today's behavior, never worse.
|
||||
- Bare home_art keys are trusted: a typo drops the home tile only.
|
||||
- Full titles map (64) stops learning with a log line.
|
||||
|
||||
## Hardware matrix (proven)
|
||||
|
||||
Gateway ready, TLS-ECDHE suite, game detection + ticking timer post-SNTP,
|
||||
mp: art serving (phone), duplicate-ID gone, evict clean-stop rotations,
|
||||
reboot recovery. Pending eyes: appdb name flip, badge render, logo tile.
|
||||
|
||||
## Readiness
|
||||
|
||||
Production for the daemon scope. Installer/PKG remains out of scope per
|
||||
directive. Ship it.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Troubleshooting
|
||||
|
||||
## `?` tile / missing art
|
||||
|
||||
1. Daemon log first: `art: resolved mp` = our side fine, look downstream.
|
||||
2. Vesktop (desktop mod) shows `?` for tiles the official clients and
|
||||
phone render correctly. Check phone before reporting.
|
||||
3. Black/blank idle tile = `home_art` empty or pointing at a dead URL.
|
||||
Set it to a live PNG (default: project PlayStation logo).
|
||||
|
||||
## Name shows raw ID (CUSA…)
|
||||
|
||||
Chain: config `titles` → app.db → local files → Sony TMDB → fallback.
|
||||
TMDB is TCP-filtered from most consoles (proven, not fixable in code).
|
||||
If app.db misses too, add one line to `titles` — or wait: first-hit
|
||||
self-learns, so one manual entry fixes a title forever.
|
||||
|
||||
## Installer opens then instantly exits
|
||||
|
||||
The first dialog was auto-dismissed (system transition). Current builds
|
||||
hide the splash + settle 3 s first. Reinstall the latest PKG.
|
||||
|
||||
## Kernel panic on installer launch (fixed)
|
||||
|
||||
Called dialogs without loading sysmodules. Fixed: module loads in
|
||||
`ui_init`, app exits quietly if they fail. If you still panic, your PKG
|
||||
predates the fix — reinstall.
|
||||
|
||||
## Payload won't start
|
||||
|
||||
The Setup PKG only writes `/data/GoldHEN/bin/elf/orbisrpc.bin` — it never
|
||||
launches anything. Start it from GoldHEN's payload menu, or enable AutoRun
|
||||
for `orbisrpc` once so it boots with every jailbreak.
|
||||
|
||||
Manual injection (see `injecting.md`) still exists and needs the loaders
|
||||
open: ports 9090/9021/9020 are closed whenever GoldHEN's BinLoader toggle
|
||||
is off. Flip it in GoldHEN's menu. Reboot wipes jailbreak + daemon
|
||||
(RAM-only), so re-jailbreak first.
|
||||
|
||||
## Token rejected (close 4004)
|
||||
|
||||
Token dead (password change / logout-all). Paste a fresh one; the daemon
|
||||
survives 4004s and picks it up without reinstall.
|
||||
|
||||
## No FTP (connection refused)
|
||||
|
||||
GoldHEN FTP toggle off, or console rebooted to stock. Re-jailbreak.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Injecting OrbisRPC into the PS4 — the complete guide
|
||||
|
||||
This exists because getting a payload to *execute* took longer than
|
||||
writing the payload. Everything below was learned on a real 9.00 console.
|
||||
|
||||
## You need first
|
||||
|
||||
- PS4 on 9.00, jailbroken with GoldHEN (2.4b18+ recommended — older
|
||||
payloader builds segfault on ELF files, see below).
|
||||
- Console and computer on the same network. Find the PS4 IP:
|
||||
Settings → Network → View Connection Status (ours is `192.168.1.136`).
|
||||
- The payload file: `build-sdk/orbisrpc_sdk.elf` (built via
|
||||
`./scripts/build_sdk.sh`).
|
||||
|
||||
## Method 1 — elfldr (recommended)
|
||||
|
||||
elfldr is a proper ELF loader that runs payloads as separate processes.
|
||||
|
||||
1. Get it listening. Either load `elfldr.elf` through GoldHEN's payloader
|
||||
page once, or keep it running — it serves on **port 9021** until reboot.
|
||||
2. Send the payload with a raw TCP connection, then close the write side:
|
||||
|
||||
```python
|
||||
import socket
|
||||
data = open("build-sdk/orbisrpc_sdk.elf", "rb").read()
|
||||
s = socket.socket()
|
||||
s.settimeout(25)
|
||||
s.connect(("192.168.1.136", 9021))
|
||||
s.sendall(data)
|
||||
try:
|
||||
s.shutdown(socket.SHUT_WR) # signals end-of-payload
|
||||
except OSError:
|
||||
pass
|
||||
s.close()
|
||||
```
|
||||
|
||||
3. Verify it runs: within ~20 seconds `/data/orbisRPC/log.txt` must contain
|
||||
`orbisRPC payload boot`. No file = it never executed.
|
||||
|
||||
## Method 2 — GoldHEN BinLoader server (port 9020)
|
||||
|
||||
The classic route (NetCat tools, phone apps, scripts all speak it).
|
||||
|
||||
1. Arm it: tap **BinLoader** in the exploit host menu (NOT the GoldHEN
|
||||
settings payloader page — different loader).
|
||||
2. Within seconds, fire blind — **one single connection carrying the full
|
||||
payload, no port check first**:
|
||||
|
||||
```python
|
||||
import socket
|
||||
data = open("build-sdk/orbisrpc_sdk.elf", "rb").read()
|
||||
s = socket.socket()
|
||||
s.settimeout(25)
|
||||
s.connect(("192.168.1.136", 9020))
|
||||
s.sendall(data)
|
||||
s.close()
|
||||
```
|
||||
|
||||
3. Same verification: look for `orbisRPC payload boot` in the log.
|
||||
|
||||
### THE ONE-SHOT RULE (read this twice)
|
||||
|
||||
GoldHEN's BinLoader arms **once**. Any empty port check (`connect_ex`,
|
||||
`nc -z`, port scanners) connects with nothing to send and **burns the
|
||||
arm** — the real payload then arrives at a dead listener. Sequence is
|
||||
always: tap → say go → fire immediately → verify. Never probe first.
|
||||
|
||||
## Method 3 — in-app injector
|
||||
|
||||
The OrbisRPC setup app injects over loopback itself
|
||||
(`127.0.0.1:9020`, fallback `9090`) from its menu. Needs a listening
|
||||
loader exactly like Method 2.
|
||||
|
||||
## Method 4 — sender page (no PC tools)
|
||||
|
||||
`https://SirHumza.github.io/orbisrpc-host/` in the PS4 browser
|
||||
auto-sends the bundled backend to the console's own loader and prints
|
||||
the result on screen.
|
||||
|
||||
## What NOT to use
|
||||
|
||||
- **GoldHEN settings payloader page**: on GoldHEN ≤ 2.4 (pre-b18.5
|
||||
lineage) its loader thread segfaults (`SIGSEGV`, null read in
|
||||
`GoldHENLoader`) on every ELF — including 90KB hello-worlds. The klog
|
||||
prints `payload launched successfully` and then dies. If you see this,
|
||||
update GoldHEN, don't rebuild the payload.
|
||||
- **Raw SELF files**: the page expects ELF (`\x7fELF`). SELF uploads
|
||||
report "invalid payload".
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Meaning | Fix |
|
||||
|---|---|---|
|
||||
| `Connection refused` on 9020/9090 | No listener armed | Tap BinLoader / open payloader page, retry instantly |
|
||||
| `payload launched successfully` then silence, no log | Loader segfault (see above) | Update GoldHEN ≥ v2.4b18.5, use BinLoader server |
|
||||
| `Error handling payload` | Loader rejected the bytes | Re-check file integrity (`shasum`), resend |
|
||||
| Log exists but `FATAL: token rejected (4004)` | Token rotated/dead | Fresh token into `/data/orbisRPC/config.json`, relaunch |
|
||||
| Multiple `Payload` processes in process list | Old instances piled up | Reboot clears them; the daemon's lock prevents recurrence |
|
||||
|
||||
## Watching it work
|
||||
|
||||
- Kernel log (loader events, faults): TCP `192.168.1.136:3232`, raw stream.
|
||||
- Daemon log: `/data/orbisRPC/log.txt` via FTP (`192.168.1.136:2121`,
|
||||
anonymous). Absent file = payload never executed, full stop.
|
||||
- Process list: `sysctl kern.proc` (see `scripts/ps4_watch.py`) — look
|
||||
for `Payload` and `eboot.bin` entries.
|
||||
- Final proof: Discord profile shows the game + ticking timer.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Findings archive
|
||||
|
||||
Hardware-grounded research from bringing OrbisRPC up on a real PS4
|
||||
(9.00, GoldHEN 2.4). Written for humans and for AI assistants joining
|
||||
later: every claim below was observed on-console unless marked THEORY.
|
||||
|
||||
- [loader.md](loader.md) — GoldHEN payloader vs BinLoader server vs
|
||||
elfldr: behaviors, crashes, one-shot rule, ports.
|
||||
- [elf-linkage.md](elf-linkage.md) — why OpenOrbis-linked binaries die
|
||||
instantly in spawned processes; NEEDED/UND analysis; SDK port status.
|
||||
- [tls-ime.md](tls-ime.md) — mbedTLS 3.x PSA failure on-console (fixed),
|
||||
IME dialog init failures (open), exact error codes seen.
|
||||
- [capture.md](capture.md) — how to observe: klog, FTP paths, ports,
|
||||
screenshot locations, what each log proves.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Observing the console (for assistants)
|
||||
|
||||
## Channels
|
||||
|
||||
| Channel | Address | Use |
|
||||
|---|---|---|
|
||||
| FTP | `192.168.1.136:2121` (anonymous) | files up/down |
|
||||
| Kernel log | `192.168.1.136:3232` (TCP stream) | loader events, faults, crashes |
|
||||
| BinLoader server | `192.168.1.136:9020` (one-shot per arm) | inject; NEVER probe first — an empty connect burns the arm, then fire blind |
|
||||
| elfldr (if running) | `192.168.1.136:9021` | third-party ELF test harness |
|
||||
|
||||
## Key paths (FTP)
|
||||
|
||||
- `/data/orbisRPC/log.txt` — daemon log. Absent = payload never ran.
|
||||
- `/data/orbisRPC/app.log` — setup-app + transport logs.
|
||||
- `/data/orbisRPC/screen.log` — every dialog as text + answers.
|
||||
- `/data/orbisRPC/diag.txt` — sanitized report (token structurally excluded).
|
||||
- `/data/GoldHEN/payloads/` — autoloaded at jailbreak.
|
||||
- `/data/payloads/` — Payload Guest scan dir.
|
||||
- `/data/pkg/` — PKG staging.
|
||||
- `/user/av_contents/photo/NPXS20001/<TITLE>/*/*/*.jpg` — screenshots.
|
||||
- `/user/data/sce_coredumps/` — crash dumps.
|
||||
|
||||
## Proof markers
|
||||
|
||||
- Daemon alive: `log.txt` exists with `daemon start` line.
|
||||
- Network proven: `tls: established (TLSv1.2)` in app.log.
|
||||
- September success marker: `gateway ready` + `presence:` lines.
|
||||
- Loader segfault signature: `signal 11 (SIGSEGV), thread GoldHENLoader`,
|
||||
fault address 0, right after `payload launched successfully`.
|
||||
|
||||
## Rules learned the hard way
|
||||
|
||||
1. Never port-check 9020/9090 before sending — fire blind on tap.
|
||||
2. Klog is a live firehose: capture across the send window, not after.
|
||||
3. `log.txt` absent always means "never executed", never "crashed later".
|
||||
4. Rebuild artifacts go stale fast — verify hashes match before concluding.
|
||||
5. **Never FTP-upload `.prx`/SELF files**: the console-side path
|
||||
deterministically converts SELF containers to raw ELF on write
|
||||
(verified: 1,476,032-byte PRX repeatedly lands as a 1,719,072-byte
|
||||
ELF, different hash; random bytes and PKGs round-trip perfectly).
|
||||
Deliver plugins via PKG install only.
|
||||
@@ -0,0 +1,39 @@
|
||||
# ELF linkage: why OpenOrbis binaries die in spawned processes
|
||||
|
||||
## Observed
|
||||
|
||||
- elfldr.elf (ps4-payload-sdk build): runs, serves, no faults.
|
||||
- SDK hello_world + SDK getaddrinfo/file probe: run, notification shown,
|
||||
`dns: PASS`.
|
||||
- Every OpenOrbis-linked binary (hello 90KB, v0.4.0, 1.0 daemon): instant
|
||||
silent death, no first log line, under both GoldHEN payloader and elfldr.
|
||||
|
||||
## Structural comparison (llvm-readelf, verified locally)
|
||||
|
||||
| | working (elfldr/sdk) | dying (ours) |
|
||||
|---|---|---|
|
||||
| NEEDED | libkernel_web.sprx, libSceLibcInternal.sprx, libSceNet.sprx | libkernel.so + app-world `.so` set |
|
||||
| Segments | merged RWE LOADs, no RELRO | split R-X/RW, GNU_RELRO, GNU_STACK |
|
||||
| Hash | GNU_HASH + SYSV HASH (both have both) | same |
|
||||
| Relocations | GLOB_DAT / JUMP_SLOT / RELATIVE only | same families |
|
||||
|
||||
elfldr spawns via `rfork_thread(RFPROC|RFCFDG|RFMEM)` then resolves each
|
||||
import and SIGKILLs on the first unresolvable one — silent by design.
|
||||
Our 89 undefined imports (vs ~26 working) include `libkernel.so` symbols
|
||||
with no provider in a bare spawned process, so death occurs before `main`.
|
||||
|
||||
## Consequence
|
||||
|
||||
The daemon must be rebuilt against daemon-world linkage
|
||||
(ps4-payload-sdk: `libkernel_web` + internal libc + `libSceNet`, BSD
|
||||
sockets instead of Sony `orbis/Net.h`). Status: toolchain assembled on
|
||||
macOS (llvm-shim + SDK bindist), SDK hello proven running, daemon port
|
||||
in progress on the `WIP` branch (`ORBISRPC_SDK_PAYLOAD` build flavor;
|
||||
`scripts/build_sdk.sh`). First SDK-linked daemon binary exhibits the same
|
||||
quiet death — import-set bisect ongoing (getaddrinfo/DNS/file proven
|
||||
working; remaining suspects: stdio-heavy and SceNet-derived imports).
|
||||
|
||||
## Ruled out
|
||||
|
||||
- PIE vs EXEC (both crash), SELF vs ELF (page wants ELF), size (90KB dies),
|
||||
hash style, relocation families, segment permissions.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Loader behavior (observed on PS4 9.00, GoldHEN 2.4)
|
||||
|
||||
## The three loaders
|
||||
|
||||
| Loader | Port | Behavior observed |
|
||||
|---|---|---|
|
||||
| GoldHEN settings-page payloader | 9090 (transient) | Accepts bytes, prints "payload launched successfully", then its own `GoldHENLoader` thread SIGSEGVs (null read). Payload never executes. Reproduced with 90KB hello-world, v0.4.0, 1.0 daemon. |
|
||||
| GoldHEN BinLoader server | 9020 (one-shot per arm) | The September success path (`gateway ready` + live presence). Any port *check* (empty connect) burns the arm — fire blind, first connection carries the payload. |
|
||||
| elfldr (ps4-payload-dev v0.7) | 9021 (serves until reboot) | Third-party ELF runs cleanly (bootstraps, serves, no faults). Our OpenOrbis-linked binaries die silently inside it. |
|
||||
|
||||
## Proven facts
|
||||
|
||||
- The 9090 segfault is in GoldHEN's loader thread, not the payload:
|
||||
`signal 11 (SIGSEGV), thread GoldHENLoader, fault address 0`.
|
||||
- ELF support in the settings-page loader needs GoldHEN ≥ v2.4b18.5;
|
||||
older builds cannot load ELFs at all.
|
||||
- 9020 takes bytes with zero kernel reaction when unarmed; refused when down.
|
||||
- FTP (2121) and klog (3232) are the observation channels; see capture.md.
|
||||
|
||||
## Open
|
||||
|
||||
- Whether a newer GoldHEN (v2.4b18+) fixes the 9090 segfault.
|
||||
- The exact GoldHEN build on the test console (About screen).
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""build_art_table.py - resolve Sony CDN artwork for a title list via TMDB.
|
||||
|
||||
Runs on the BUILD host (console network blocks TMDB port 80) and emits
|
||||
orbisrpc/art_table.h: {titleId, name, icon_url} for every resolvable title.
|
||||
Also downloads each icon into icons/ for the art_base_url pack.
|
||||
|
||||
Usage: python3 scripts/build_art_table.py titles.txt [--icons icons/]
|
||||
Title list: one TITLEID per line (CUSAxxxxx etc.); unknown IDs are skipped.
|
||||
"""
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
KEY = bytes([
|
||||
0xF5, 0xDE, 0x66, 0xD2, 0x68, 0x0E, 0x25, 0x5B, 0x2D, 0xF7, 0x9E, 0x74,
|
||||
0xF8, 0x90, 0xEB, 0xF3, 0x49, 0x26, 0x2F, 0x61, 0x8B, 0xCA, 0xE2, 0xA9,
|
||||
0xAC, 0xCD, 0xEE, 0x51, 0x56, 0xCE, 0x8D, 0xF2, 0xCD, 0xF2, 0xD4, 0x8C,
|
||||
0x71, 0x17, 0x3C, 0xDC, 0x25, 0x94, 0x46, 0x5B, 0x87, 0x40, 0x5D, 0x19,
|
||||
0x7C, 0xF1, 0xAE, 0xD3, 0xB7, 0xE9, 0x67, 0x1E, 0xEB, 0x56, 0xCA, 0x67,
|
||||
0x53, 0xC2, 0xE6, 0xB0,
|
||||
])
|
||||
TMDB = "http://tmdb.np.dl.playstation.net"
|
||||
|
||||
|
||||
def c_escape(s):
|
||||
return s.replace("\\", "\\\\").replace('"', '\\"')
|
||||
|
||||
|
||||
def resolve(tid):
|
||||
mac = hmac.new(KEY, (tid + "_00\x00").encode("latin1")[:12],
|
||||
hashlib.sha1).hexdigest().upper()
|
||||
url = f"{TMDB}/tmdb2/{tid}_00_{mac}/{tid}_00.json"
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "orbisRPC-arttbl"})
|
||||
d = json.loads(urllib.request.urlopen(req, timeout=20).read())
|
||||
name = d["names"][0]["name"]
|
||||
icon = d["icons"][0]["icon"].replace("http://", "https://")
|
||||
return name, icon
|
||||
|
||||
|
||||
def main():
|
||||
titles = [l.strip() for l in open(sys.argv[1]) if l.strip()]
|
||||
icons = sys.argv[3] if len(sys.argv) > 3 and sys.argv[2] == "--icons" else None
|
||||
if icons:
|
||||
os.makedirs(icons, exist_ok=True)
|
||||
rows = []
|
||||
for tid in titles:
|
||||
try:
|
||||
name, icon = resolve(tid)
|
||||
except Exception as e:
|
||||
print(f"MISS {tid}: {str(e)[:60]}", flush=True)
|
||||
continue
|
||||
print(f"OK {tid} :: {name}", flush=True)
|
||||
rows.append((tid, name, icon))
|
||||
if icons:
|
||||
try:
|
||||
img = urllib.request.urlopen(icon, timeout=25).read()
|
||||
open(os.path.join(icons, tid.lower() + ".png"), "wb").write(img)
|
||||
except Exception as e:
|
||||
print(f" icon fetch failed: {str(e)[:60]}", flush=True)
|
||||
with open("orbisrpc/art_table.h", "w") as f:
|
||||
f.write("/* art_table.h - GENERATED by scripts/build_art_table.py.\n")
|
||||
f.write(" * Do not edit: Sony CDN artwork baked at build time so the\n")
|
||||
f.write(" * console needs no TMDB network access for art.\n")
|
||||
f.write(f" * Titles: {len(rows)}. */\n")
|
||||
f.write("#ifndef ORBISRPC_ART_TABLE_H\n#define ORBISRPC_ART_TABLE_H\n")
|
||||
f.write("#include <stddef.h>\n")
|
||||
f.write("typedef struct { const char *id; const char *name;"
|
||||
" const char *icon; } art_entry_t;\n")
|
||||
f.write("static const art_entry_t ORBISRPC_ART_TABLE[] = {\n")
|
||||
for tid, name, icon in rows:
|
||||
f.write(f' {{ "{tid}", "{c_escape(name)}", "{icon}" }},\n')
|
||||
f.write("};\n#define ORBISRPC_ART_TABLE_N "
|
||||
f"(sizeof ORBISRPC_ART_TABLE / sizeof ORBISRPC_ART_TABLE[0])\n")
|
||||
f.write("#endif\n")
|
||||
print(f"wrote art_table.h with {len(rows)} entries")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_nametable.py - build orbisrpc/nametable.h from config/titles.json.
|
||||
|
||||
titles.json maps TITLEID -> display name (extracted from the PS4's
|
||||
/system_data/priv/mms/app.db tbl_appbrowse_* tables, or extended by hand).
|
||||
The generated table is compiled in, so name lookup needs no filesystem
|
||||
access and works inside any console sandbox.
|
||||
|
||||
Usage: python3 scripts/gen_nametable.py
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SRC = os.path.join(ROOT, "config", "titles.json")
|
||||
DST = os.path.join(ROOT, "orbisrpc", "nametable.h")
|
||||
|
||||
|
||||
def c_escape(s):
|
||||
out = []
|
||||
for ch in s:
|
||||
o = ord(ch)
|
||||
if ch == '"':
|
||||
out.append('\\"')
|
||||
elif ch == "\\":
|
||||
out.append("\\\\")
|
||||
elif 32 <= o < 127:
|
||||
out.append(ch)
|
||||
else:
|
||||
# octal: unlike \x, always stops after 3 digits, so a hex
|
||||
# digit following the escape can never merge into it.
|
||||
for b in ch.encode("utf-8"):
|
||||
out.append("\\%03o" % b)
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def main():
|
||||
with open(SRC, encoding="utf-8") as fh:
|
||||
titles = json.load(fh)
|
||||
items = sorted(titles.items())
|
||||
with open(DST, "w", encoding="utf-8") as fh:
|
||||
fh.write("/* nametable.h - GENERATED by scripts/gen_nametable.py. Do not edit. */\n")
|
||||
fh.write("#ifndef NAMETABLE_H\n#define NAMETABLE_H\n");
|
||||
fh.write("#include <stddef.h>\n#include <string.h>\n")
|
||||
fh.write("typedef struct { const char *id; const char *name; } nt_entry_t;\n")
|
||||
fh.write("static const nt_entry_t g_nametable[] = {\n")
|
||||
for tid, name in items:
|
||||
fh.write(' { "%s", "%s" },\n' % (c_escape(tid), c_escape(name)))
|
||||
fh.write("};\n")
|
||||
fh.write("static const size_t g_nametable_count = sizeof g_nametable / sizeof g_nametable[0];\n")
|
||||
fh.write("static int nametable_lookup(const char *tid, char *out, size_t cap){\n")
|
||||
fh.write(" if(!tid || !out || cap == 0) return -1;\n")
|
||||
fh.write(" for(size_t i = 0; i < g_nametable_count; i++){\n")
|
||||
fh.write(" if(!strcmp(tid, g_nametable[i].id)){\n")
|
||||
fh.write(" strncpy(out, g_nametable[i].name, cap - 1);\n")
|
||||
fh.write(" out[cap - 1] = 0;\n")
|
||||
fh.write(" return out[0] ? 0 : -1;\n")
|
||||
fh.write(" }\n")
|
||||
fh.write(" }\n")
|
||||
fh.write(" return -1;\n")
|
||||
fh.write("}\n#endif\n")
|
||||
print("wrote %s with %d entries" % (DST, len(items)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,37 @@
|
||||
APOL00004
|
||||
CHTM00777
|
||||
CUSA00021
|
||||
CUSA00127
|
||||
CUSA00135
|
||||
CUSA00411
|
||||
CUSA00740
|
||||
CUSA01004
|
||||
CUSA01015
|
||||
CUSA01623
|
||||
CUSA01839
|
||||
CUSA03245
|
||||
CUSA04294
|
||||
CUSA04602
|
||||
CUSA06014
|
||||
CUSA06545
|
||||
CUSA06712
|
||||
CUSA08992
|
||||
CUSA09311
|
||||
CUSA11995
|
||||
CUSA13323
|
||||
CUSA14909
|
||||
CUSA18795
|
||||
CUSA20177
|
||||
CUSA20499
|
||||
CUSA24899
|
||||
CUSA26618
|
||||
CUSA33151
|
||||
FLTZ00003
|
||||
GBTX00001
|
||||
LAPY20006
|
||||
LAPY20009
|
||||
LAPY20015
|
||||
MANU90003
|
||||
NPXS39041
|
||||
PKGI13337
|
||||
CUSA09303
|
||||
@@ -0,0 +1,62 @@
|
||||
# TLS + IME + presence-display findings (hardware-verified)
|
||||
|
||||
## Presence display rules (verified 2026-09-23 via live holds + human eyes)
|
||||
|
||||
- Game activity with **external-URL art or dangling asset key**: Discord
|
||||
drops the ENTIRE activity (name, timer, everything). No error anywhere.
|
||||
- Game activity with **live app id, no art**: renders fully
|
||||
(name/details/state/timer).
|
||||
- Game activity with **live app id + uploaded asset key**: renders; tile
|
||||
shows "?" until the asset propagates/caches clear (can take a restart).
|
||||
- Bare name activity: renders.
|
||||
- Custom status (type 4): renders.
|
||||
- `since: null` + integer-ms `start` required (0 wedges timer at 0:00).
|
||||
- Asset uploads ARE possible via API: stage file through
|
||||
`POST /channels/{id}/attachments`, PUT bytes to `upload_url`, then
|
||||
`POST /applications/{app}/assets` with `key` + `upload_filename`.
|
||||
New assets report `"visibility": "private"`; display still pending
|
||||
verification after propagation.
|
||||
- Lanyard is BLIND to user-token gateway activities (shows [] even for
|
||||
live-rendered ones). Human eyes only. Do not verify with it.
|
||||
|
||||
Symptom: every handshake died instantly with return `-1` (not a real
|
||||
mbedTLS code), after DNS + TCP succeeded.
|
||||
|
||||
Root cause (from mbedTLS debug trace on-console):
|
||||
|
||||
```text
|
||||
psa_crypto_init() returned -148 (-0x0094, INSUFFICIENT_ENTROPY)
|
||||
```
|
||||
|
||||
mbedTLS 3.x routes TLS 1.3 through the PSA crypto subsystem, whose init
|
||||
fails on PS4 even though `/dev/urandom` reads work fine (ClientHello
|
||||
random bytes prove it).
|
||||
|
||||
Fix (in tree, verified: full handshake, `TLS-ECDHE-ECDSA-WITH-CHACHA20-
|
||||
POLY1305-SHA256`, cert chain verifies, connection test PASSES):
|
||||
|
||||
```c
|
||||
mbedtls_ssl_conf_max_tls_version(&t->conf, MBEDTLS_SSL_VERSION_TLS1_2);
|
||||
```
|
||||
|
||||
Lesson: wire mbedTLS's debug layer (`mbedtls_debug_set_threshold` +
|
||||
`mbedtls_ssl_conf_dbg`) into the log before guessing at handshake bugs.
|
||||
|
||||
## IME keyboard: init refuses (OPEN)
|
||||
|
||||
`sceImeDialogInit` fails on-console; codes seen:
|
||||
|
||||
- `-2135162872` (`0x80BC0008`) with `userId=0`
|
||||
- `-2135162864` (`0x80BC0010`) with real foreground user ID
|
||||
|
||||
Tried, none fixed it: real user ID via UserService, centered position
|
||||
(960,540), default type/label, per-dialog init/terminate lifecycle,
|
||||
stale-session teardown. Reference: Apollo PS4 dialog.c pattern mirrored.
|
||||
Workaround in place: token pre-seeded via config; IME still unresolved.
|
||||
|
||||
## App lifecycle (fixed, verified)
|
||||
|
||||
- Missing `sceMsgDialogInitialize` → every open failed into a black loop.
|
||||
Fixed per Apollo pattern (init + terminate per dialog).
|
||||
- 30s dialog watchdog blanked idle screens — removed, blocking waits now.
|
||||
- Exit path terminates dialogs, unloads modules, `exit(0)`.
|
||||
@@ -0,0 +1,84 @@
|
||||
# orbisRPC Setup installer (OpenOrbis app).
|
||||
# Flow: daemon payload -> evict old -> token -> done.
|
||||
# Payload Guest reads /data/payloads/ on this console.
|
||||
TITLE := orbisRPC Setup
|
||||
VERSION := 1.0.0
|
||||
TITLE_ID := ORPC00001
|
||||
CONTENT_ID := IV0000-ORPC00001_00-ORBISRPCSETUP000
|
||||
|
||||
LIBS := -lc -lkernel -lc++ -lSceMsgDialog -lSceCommonDialog -lSceImeDialog -lSceSysmodule -lSceNet -lSceUserService -lSceSystemService -lScePad
|
||||
|
||||
ASSETS := $(wildcard installer/assets/**/*)
|
||||
LIBMODULES := $(wildcard installer/sce_module/*)
|
||||
|
||||
TOOLCHAIN ?= $(OO_PS4_TOOLCHAIN)
|
||||
ifeq ($(TOOLCHAIN),)
|
||||
TOOLCHAIN := /Users/mac/PS4Toolchain/OpenOrbis/PS4Toolchain
|
||||
endif
|
||||
PROJDIR := installer
|
||||
COMMONDIR := $(TOOLCHAIN)/samples/_common
|
||||
INTDIR := $(PROJDIR)/x64/Debug
|
||||
|
||||
CFILES := $(wildcard $(PROJDIR)/*.c)
|
||||
CPPFILES := $(wildcard $(PROJDIR)/*.cpp)
|
||||
OBJS := $(patsubst $(PROJDIR)/%.c, $(INTDIR)/%.o, $(CFILES)) $(patsubst $(PROJDIR)/%.cpp, $(INTDIR)/%.o, $(CPPFILES)) $(INTDIR)/jsonlite.o
|
||||
|
||||
# llvmshim (no brew): export PATH="$HOME/llvmshim:$PATH" before make.
|
||||
LLVM := $(HOME)/llvmshim
|
||||
CC := $(LLVM)/clang
|
||||
CCX := $(LLVM)/clang++
|
||||
LD := $(LLVM)/ld.lld
|
||||
CDIR := macos
|
||||
|
||||
CFLAGS := -DINSTALLER_PS4 -DSETUP_VERSION='"$(VERSION)"' --target=x86_64-pc-freebsd12-elf -fPIC -funwind-tables -c -isysroot $(TOOLCHAIN) -isystem $(TOOLCHAIN)/include -Iorbisrpc -Iinstaller
|
||||
CXXFLAGS := $(CFLAGS) -isystem $(TOOLCHAIN)/include/c++/v1
|
||||
LDFLAGS := -m elf_x86_64 -pie --script $(TOOLCHAIN)/link.x --eh-frame-hdr -L$(TOOLCHAIN)/lib $(LIBS) $(TOOLCHAIN)/lib/crt1.o -L$(TOOLCHAIN)/lib
|
||||
|
||||
_unused := $(shell mkdir -p $(INTDIR))
|
||||
|
||||
all: $(CONTENT_ID).pkg
|
||||
|
||||
$(CONTENT_ID).pkg: installer/pkg.gp4
|
||||
cd installer && $(TOOLCHAIN)/bin/$(CDIR)/PkgTool.Core pkg_build pkg.gp4 . && mv $(CONTENT_ID).pkg ..
|
||||
|
||||
installer/pkg.gp4: installer/eboot.bin installer/assets/daemon.elf installer/assets/evict.elf installer/assets/config.json installer/sce_sys/about/right.sprx installer/sce_sys/param.sfo installer/sce_sys/icon0.png installer/sce_sys/pic1.png $(LIBMODULES) $(ASSETS)
|
||||
cd installer && $(TOOLCHAIN)/bin/$(CDIR)/create-gp4 -out pkg.gp4 --content-id=$(CONTENT_ID) --files "eboot.bin assets/daemon.elf assets/evict.elf assets/config.json sce_sys/about/right.sprx sce_sys/param.sfo sce_sys/icon0.png sce_sys/pic1.png sce_module/libSceFios2.prx sce_module/libc.prx $(patsubst installer/%,%,$(ASSETS))"
|
||||
|
||||
installer/sce_sys/param.sfo: installer/Makefile
|
||||
$(TOOLCHAIN)/bin/$(CDIR)/PkgTool.Core sfo_new $@
|
||||
$(TOOLCHAIN)/bin/$(CDIR)/PkgTool.Core sfo_setentry $@ APP_TYPE --type Integer --maxsize 4 --value 1
|
||||
$(TOOLCHAIN)/bin/$(CDIR)/PkgTool.Core sfo_setentry $@ APP_VER --type Utf8 --maxsize 8 --value '$(VERSION)'
|
||||
$(TOOLCHAIN)/bin/$(CDIR)/PkgTool.Core sfo_setentry $@ ATTRIBUTE --type Integer --maxsize 4 --value 0
|
||||
$(TOOLCHAIN)/bin/$(CDIR)/PkgTool.Core sfo_setentry $@ CATEGORY --type Utf8 --maxsize 4 --value 'gd'
|
||||
$(TOOLCHAIN)/bin/$(CDIR)/PkgTool.Core sfo_setentry $@ CONTENT_ID --type Utf8 --maxsize 48 --value '$(CONTENT_ID)'
|
||||
$(TOOLCHAIN)/bin/$(CDIR)/PkgTool.Core sfo_setentry $@ DOWNLOAD_DATA_SIZE --type Integer --maxsize 4 --value 0
|
||||
$(TOOLCHAIN)/bin/$(CDIR)/PkgTool.Core sfo_setentry $@ SYSTEM_VER --type Integer --maxsize 4 --value 0
|
||||
$(TOOLCHAIN)/bin/$(CDIR)/PkgTool.Core sfo_setentry $@ TITLE --type Utf8 --maxsize 128 --value '$(TITLE)'
|
||||
$(TOOLCHAIN)/bin/$(CDIR)/PkgTool.Core sfo_setentry $@ TITLE_ID --type Utf8 --maxsize 12 --value '$(TITLE_ID)'
|
||||
$(TOOLCHAIN)/bin/$(CDIR)/PkgTool.Core sfo_setentry $@ VERSION --type Utf8 --maxsize 8 --value '$(VERSION)'
|
||||
|
||||
installer/eboot.bin: $(OBJS)
|
||||
$(LD) $(INTDIR)/*.o -o $(INTDIR)/$(PROJDIR).elf $(LDFLAGS)
|
||||
$(TOOLCHAIN)/bin/$(CDIR)/create-fself-macos -in=$(INTDIR)/$(PROJDIR).elf -out=$(INTDIR)/$(PROJDIR).oelf --eboot "installer/eboot.bin" --paid 0x3800000000000011
|
||||
|
||||
$(INTDIR)/%.o: $(PROJDIR)/%.c
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
$(INTDIR)/jsonlite.o: orbisrpc/jsonlite.c
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
# Stage the daemon + evict payloads + config into the app image.
|
||||
# config.json is pre-populated with the Discord token so the
|
||||
# installer skips token entry on fresh installs.
|
||||
installer/assets/daemon.elf: build-sdk/orbisrpc_sdk.elf
|
||||
cp $< $@
|
||||
installer/assets/evict.elf: build-sdk/evict.elf
|
||||
cp $< $@
|
||||
installer/assets/config.json: installer/config.json
|
||||
cp $< $@
|
||||
|
||||
$(INTDIR)/%.o: $(PROJDIR)/%.cpp
|
||||
$(CCX) $(CXXFLAGS) -o $@ $<
|
||||
|
||||
clean:
|
||||
rm -rf $(INTDIR) installer/eboot.bin installer/pkg.gp4 $(CONTENT_ID).pkg installer/sce_sys/param.sfo installer/assets/daemon.elf installer/assets/evict.elf installer/assets/config.json
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":1,"token":"SET_ME","presence_state":"On PS4"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":1,"token":"SET_ME","presence_state":"On PS4"}
|
||||
@@ -0,0 +1,193 @@
|
||||
/* icfg.c - atomic read-modify-write over the daemon config. */
|
||||
#include "icfg.h"
|
||||
#include "jsonlite.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define ICFG_MAX (64u*1024u)
|
||||
|
||||
int token_valid(const char *t){
|
||||
size_t n, i;
|
||||
int dots = 0;
|
||||
if(!t) return 0;
|
||||
n = strlen(t);
|
||||
if(n < 40 || n > 150) return 0;
|
||||
if(!strcmp(t, "SET_ME") || !strcmp(t, "PUT_TOKEN_HERE")) return 0;
|
||||
for(i = 0; i < n; i++){
|
||||
char c = t[i];
|
||||
if(c == '.'){ dots++; continue; }
|
||||
if(!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||
(c >= '0' && c <= '9') || c == '_' || c == '-'))
|
||||
return 0;
|
||||
}
|
||||
return dots >= 2;
|
||||
}
|
||||
|
||||
static jl_val_t *icfg_read(const char *path){
|
||||
FILE *f = fopen(path, "rb");
|
||||
long sz;
|
||||
char *buf;
|
||||
jl_val_t *r;
|
||||
if(!f) return NULL;
|
||||
if(fseek(f, 0, SEEK_END) != 0){ fclose(f); return NULL; }
|
||||
sz = ftell(f);
|
||||
if(sz <= 0 || sz > (long)ICFG_MAX){ fclose(f); return NULL; }
|
||||
if(fseek(f, 0, SEEK_SET) != 0){ fclose(f); return NULL; }
|
||||
buf = (char *)malloc((size_t)sz + 1);
|
||||
if(!buf){ fclose(f); return NULL; }
|
||||
if(fread(buf, 1, (size_t)sz, f) != (size_t)sz){
|
||||
free(buf); fclose(f); return NULL;
|
||||
}
|
||||
fclose(f);
|
||||
buf[sz] = 0;
|
||||
r = jl_parse(buf, (size_t)sz);
|
||||
free(buf);
|
||||
if(!r || r->type != JL_OBJECT){ if(r) jl_free(r); return NULL; }
|
||||
return r;
|
||||
}
|
||||
|
||||
static int icfg_write(const char *path, jl_val_t *r){
|
||||
char tmp[300];
|
||||
char *s;
|
||||
FILE *f;
|
||||
int ok = 1;
|
||||
if(snprintf(tmp, sizeof tmp, "%s.new", path) <= 0 ||
|
||||
(size_t)strlen(path) + 5 >= sizeof tmp)
|
||||
return -1;
|
||||
s = jl_stringify(r);
|
||||
if(!s) return -1;
|
||||
f = fopen(tmp, "wb");
|
||||
if(!f){ free(s); return -1; }
|
||||
if(fputs(s, f) < 0) ok = 0;
|
||||
if(ok){
|
||||
int fd = fileno(f);
|
||||
if(fd >= 0 && fsync(fd) != 0) ok = 0;
|
||||
}
|
||||
if(ok && fclose(f) != 0) ok = 0;
|
||||
free(s);
|
||||
if(!ok){ remove(tmp); return -1; }
|
||||
if(rename(tmp, path) != 0){ remove(tmp); return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
int icfg_token_load(const char *path, char *out, size_t cap){
|
||||
jl_val_t *r = icfg_read(path);
|
||||
const jl_val_t *v;
|
||||
if(!out || cap == 0) return -1;
|
||||
out[0] = 0;
|
||||
if(!r) return -1;
|
||||
v = jl_obj_get(r, "token");
|
||||
if(v && v->type == JL_STRING && v->str){
|
||||
strncpy(out, v->str, cap - 1);
|
||||
out[cap - 1] = 0;
|
||||
}
|
||||
jl_free(r);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int icfg_token_save(const char *path, const char *token){
|
||||
jl_val_t *r;
|
||||
int rc;
|
||||
char back[160];
|
||||
if(!token_valid(token)) return -1;
|
||||
r = icfg_read(path);
|
||||
if(!r){
|
||||
r = jl_new_object();
|
||||
if(!r) return -2;
|
||||
}
|
||||
jl_obj_set(r, "token", jl_new_string(token));
|
||||
rc = icfg_write(path, r);
|
||||
jl_free(r);
|
||||
if(rc != 0) return -2;
|
||||
/* Read-back proof: an OOM inside the JSON build saves silently
|
||||
* WITHOUT the key. Never report success on faith. */
|
||||
back[0] = 0;
|
||||
if(icfg_token_load(path, back, sizeof back) != 0 ||
|
||||
strcmp(back, token) != 0)
|
||||
return -2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int icfg_set_str(const char *path, const char *key, const char *val){
|
||||
jl_val_t *r;
|
||||
int rc;
|
||||
char back[256];
|
||||
if(!key || !key[0] || !val) return -1;
|
||||
r = icfg_read(path);
|
||||
if(!r){
|
||||
r = jl_new_object();
|
||||
if(!r) return -1;
|
||||
}
|
||||
jl_obj_set(r, key, jl_new_string(val));
|
||||
rc = icfg_write(path, r);
|
||||
jl_free(r);
|
||||
if(rc != 0) return -1;
|
||||
back[0] = 0;
|
||||
if(icfg_get_str(path, key, back, sizeof back) != 0 ||
|
||||
strcmp(back, val) != 0)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int icfg_set_int(const char *path, const char *key, long val){
|
||||
jl_val_t *r;
|
||||
int rc;
|
||||
long back = 0;
|
||||
if(!key || !key[0]) return -1;
|
||||
r = icfg_read(path);
|
||||
if(!r){
|
||||
r = jl_new_object();
|
||||
if(!r) return -1;
|
||||
}
|
||||
jl_obj_set(r, key, jl_new_int(val));
|
||||
rc = icfg_write(path, r);
|
||||
jl_free(r);
|
||||
if(rc != 0) return -1;
|
||||
if(icfg_get_int(path, key, &back) != 0 || back != val) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int icfg_get_str(const char *path, const char *key, char *out, size_t cap){
|
||||
jl_val_t *r = icfg_read(path);
|
||||
const jl_val_t *v;
|
||||
if(!out || cap == 0) return -1;
|
||||
out[0] = 0;
|
||||
if(!r || !key){ if(r) jl_free(r); return -1; }
|
||||
v = jl_obj_get(r, key);
|
||||
if(v && v->type == JL_STRING && v->str){
|
||||
strncpy(out, v->str, cap - 1);
|
||||
out[cap - 1] = 0;
|
||||
}
|
||||
jl_free(r);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int icfg_titles_count(const char *path){
|
||||
jl_val_t *r = icfg_read(path);
|
||||
const jl_val_t *t;
|
||||
int n = 0;
|
||||
if(!r) return 0;
|
||||
t = jl_obj_get(r, "titles");
|
||||
if(t && t->type == JL_OBJECT){
|
||||
for(jl_val_t *p = t->child; p; p = p->next) n++;
|
||||
}
|
||||
jl_free(r);
|
||||
return n;
|
||||
}
|
||||
|
||||
int icfg_get_int(const char *path, const char *key, long *out){
|
||||
jl_val_t *r = icfg_read(path);
|
||||
const jl_val_t *v;
|
||||
if(!out) return -1;
|
||||
if(!r || !key){ if(r) jl_free(r); return -1; }
|
||||
v = jl_obj_get(r, key);
|
||||
if(v && v->type == JL_NUMBER){
|
||||
*out = v->num_is_int ? (long)v->inum : (long)v->num;
|
||||
jl_free(r);
|
||||
return 0;
|
||||
}
|
||||
jl_free(r);
|
||||
return -1;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/* icfg.h - installer config read/write for /data/orbisRPC/config.json.
|
||||
* Read-modify-write: preserves learned titles and every other field.
|
||||
* Pure logic (token_valid) is host-tested. */
|
||||
#ifndef INSTALLER_ICFG_H
|
||||
#define INSTALLER_ICFG_H
|
||||
#include <stddef.h>
|
||||
|
||||
#define ICFG_PATH "/data/orbisRPC/config.json"
|
||||
|
||||
/* 1 valid user token, 0 not. Tokens are dot-separated base64-ish,
|
||||
* 40..150 chars. Rejects placeholders and whitespace. */
|
||||
int token_valid(const char *t);
|
||||
|
||||
/* Load current token ("" when missing). 0 ok, -1 unreadable. */
|
||||
int icfg_token_load(const char *path, char *out, size_t cap);
|
||||
/* Set token (validated first). 0 saved, -1 invalid, -2 I/O error. */
|
||||
int icfg_token_save(const char *path, const char *token);
|
||||
/* Set a string/int key, preserving everything else. 0 saved, -1 I/O. */
|
||||
int icfg_set_str(const char *path, const char *key, const char *val);
|
||||
int icfg_set_int(const char *path, const char *key, long val);
|
||||
/* Read a string/int key. 0 found, -1 missing/unreadable. */
|
||||
int icfg_get_str(const char *path, const char *key, char *out, size_t cap);
|
||||
int icfg_get_int(const char *path, const char *key, long *out);
|
||||
/* Count entries in the "titles" map (learned + manual). Fail-soft 0. */
|
||||
int icfg_titles_count(const char *path);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,345 @@
|
||||
/* installer.c - clean installer, one flow forward.
|
||||
* Payload placement is the whole job: copy to /data/payloads.
|
||||
* Payload Guest reads this directory. */
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <signal.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
#include <orbis/libkernel.h>
|
||||
#include <orbis/SystemService.h>
|
||||
#include "ui.h"
|
||||
#include "icfg.h"
|
||||
|
||||
#ifndef SETUP_VERSION
|
||||
#define SETUP_VERSION "1.0.0"
|
||||
#endif
|
||||
#define DAEMON_ELF "/app0/assets/daemon.elf"
|
||||
#define EVICT_ELF "/app0/assets/evict.elf"
|
||||
#define INST_DIR "/data/orbisRPC"
|
||||
#define INST_LOG "/data/orbisRPC/install.log"
|
||||
#define PAYLOAD_BIN "/data/payloads/orbisrpc.bin"
|
||||
#define EVICT_BIN "/data/payloads/evict.elf"
|
||||
|
||||
/* Stage log: every copy step records errno + sizes to a file we can read
|
||||
* back over FTP. The dialog alone can't say WHICH stage failed. */
|
||||
static const char *g_stage = "-";
|
||||
|
||||
static void ilog(const char *tag, int err, long expect, long got){
|
||||
FILE *f;
|
||||
g_stage = tag;
|
||||
f = fopen(INST_LOG, "a");
|
||||
if(!f) f = fopen("/data/install.log", "a"); /* fallback if sandbox blocks */
|
||||
if(!f) return;
|
||||
fprintf(f, "%ld %s errno=%d(%s) expect=%ld got=%ld\n",
|
||||
(long)time(NULL), tag, err, err ? strerror(err) : "ok", expect, got);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* Stage marker without errno semantics (rc values, not errnos). */
|
||||
static void ilogv(const char *tag, long a, long b){
|
||||
FILE *f;
|
||||
g_stage = tag;
|
||||
f = fopen(INST_LOG, "a");
|
||||
if(!f) f = fopen("/data/install.log", "a");
|
||||
if(!f) return;
|
||||
fprintf(f, "%ld %s a=%ld b=%ld\n", (long)time(NULL), tag, a, b);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* --- crash guard ----------------------------------------------------
|
||||
* The wizard has died twice mid-flow with Sony's crash reporter eating
|
||||
* the core file, so the fault address never reached us. These handlers
|
||||
* run async-signal-safe only (no stdio, no malloc): they append the
|
||||
* signal, faulting address and current stage to install.log, then exit
|
||||
* cleanly. A crash becomes evidence instead of a crash dialog. */
|
||||
static volatile sig_atomic_t g_crashing = 0;
|
||||
|
||||
static size_t h_s(char *b, size_t cap, size_t p, const char *s){
|
||||
while(*s && p + 1 < cap) b[p++] = *s++;
|
||||
return p;
|
||||
}
|
||||
static size_t h_u(char *b, size_t cap, size_t p, unsigned long v){
|
||||
char t[24];
|
||||
int n = 0;
|
||||
if(v == 0) t[n++] = '0';
|
||||
while(v && n < 24){ t[n++] = (char)('0' + v % 10); v /= 10; }
|
||||
while(n && p + 1 < cap) b[p++] = t[--n];
|
||||
return p;
|
||||
}
|
||||
static size_t h_x(char *b, size_t cap, size_t p, unsigned long long v){
|
||||
static const char hx[] = "0123456789abcdef";
|
||||
char t[20];
|
||||
int n = 0;
|
||||
if(v == 0) t[n++] = '0';
|
||||
while(v && n < 20){ t[n++] = hx[v & 15]; v >>= 4; }
|
||||
while(n && p + 1 < cap) b[p++] = t[--n];
|
||||
return p;
|
||||
}
|
||||
|
||||
static void crash_handler(int sig, siginfo_t *si, void *uc){
|
||||
char b[200];
|
||||
size_t p = 0;
|
||||
int fd;
|
||||
ssize_t w;
|
||||
(void)uc;
|
||||
if(g_crashing) _exit(99);
|
||||
g_crashing = 1;
|
||||
p = h_s(b, sizeof b, p, "CRASH sig=");
|
||||
p = h_u(b, sizeof b, p, (unsigned)sig);
|
||||
p = h_s(b, sizeof b, p, " addr=0x");
|
||||
p = h_x(b, sizeof b, p, si ? (unsigned long long)(uintptr_t)si->si_addr : 0);
|
||||
p = h_s(b, sizeof b, p, " stage=");
|
||||
p = h_s(b, sizeof b, p, g_stage ? g_stage : "-");
|
||||
if(p + 1 < sizeof b) b[p++] = '\n';
|
||||
fd = open(INST_LOG, O_WRONLY | O_APPEND | O_CREAT, 0666);
|
||||
if(fd < 0) fd = open("/data/install.log", O_WRONLY | O_APPEND | O_CREAT, 0666);
|
||||
if(fd >= 0){ w = write(fd, b, p); (void)w; close(fd); }
|
||||
_exit(99);
|
||||
}
|
||||
|
||||
static void crash_guard(void){
|
||||
static const int sigs[] = { SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGABRT, SIGTRAP };
|
||||
struct sigaction sa;
|
||||
size_t i;
|
||||
memset(&sa, 0, sizeof sa);
|
||||
/* Header bug: the sa_sigaction macro expands to a member that does
|
||||
* not exist, so address the union directly. */
|
||||
sa.__sa_handler.__sa_sigaction = crash_handler;
|
||||
sa.sa_flags = SA_SIGINFO;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
for(i = 0; i < sizeof sigs / sizeof sigs[0]; i++)
|
||||
sigaction(sigs[i], &sa, NULL);
|
||||
}
|
||||
|
||||
/* Preflight: the payload must exist inside this package. A PKG built
|
||||
* or installed without staged assets fails here with a precise message
|
||||
* instead of a mysterious write error three steps later. */
|
||||
static int step_assets(void){
|
||||
FILE *a = fopen(DAEMON_ELF, "rb");
|
||||
if(a) fclose(a);
|
||||
if(!a){
|
||||
ui_ok("This install is missing its payload.\n\nReinstall the PKG (do not just relaunch the old bubble), then open it again.");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The sandbox makes stat()/fstat() lie about sizes (observed: 4096/4160
|
||||
* for a 2071552-byte file, which made a perfect copy report "denied").
|
||||
* open/read/write are truthful, so the proof is: count and hash what we
|
||||
* wrote, then count and hash what we read back. No stat anywhere. */
|
||||
static unsigned long long fnv1a(const unsigned char *p, size_t n,
|
||||
unsigned long long h){
|
||||
size_t i;
|
||||
for(i = 0; i < n; i++){
|
||||
h ^= p[i];
|
||||
h *= 1099511628211ULL;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
static int copy_file(const char *src, const char *dst){
|
||||
FILE *in, *out;
|
||||
static unsigned char buf[65536];
|
||||
size_t n;
|
||||
long written = 0, reread = 0;
|
||||
unsigned long long hw = 14695981039346656037ULL, hr = 14695981039346656037ULL;
|
||||
char detail[96];
|
||||
|
||||
in = fopen(src, "rb");
|
||||
if(!in){ ilog("src-open", errno, -1, -1); return -1; }
|
||||
out = fopen(dst, "wb");
|
||||
if(!out){ ilog("dst-open", errno, -1, -1); fclose(in); return -1; }
|
||||
while((n = fread(buf, 1, sizeof buf, in)) > 0){
|
||||
if(fwrite(buf, 1, n, out) != n){
|
||||
ilog("dst-write", errno, written, -1);
|
||||
fclose(in); fclose(out); return -1;
|
||||
}
|
||||
hw = fnv1a(buf, n, hw);
|
||||
written += (long)n;
|
||||
}
|
||||
fclose(in);
|
||||
if(fclose(out) != 0){ ilog("dst-close", errno, written, -1); return -1; }
|
||||
if(written <= 0){ ilog("src-empty", 0, written, -1); return -1; }
|
||||
|
||||
/* Read-back proof: reopen and re-count/re-hash the destination. */
|
||||
in = fopen(dst, "rb");
|
||||
if(!in){ ilog("verify-open", errno, written, -1); return -1; }
|
||||
while((n = fread(buf, 1, sizeof buf, in)) > 0){
|
||||
hr = fnv1a(buf, n, hr);
|
||||
reread += (long)n;
|
||||
}
|
||||
fclose(in);
|
||||
snprintf(detail, sizeof detail, "h=%016llx/%016llx", hw, hr);
|
||||
if(written == reread && hw == hr){
|
||||
ilog("copy-ok", 0, written, reread);
|
||||
return 0;
|
||||
}
|
||||
ilog("verify-mismatch", 0, written, reread);
|
||||
ilog(detail, 0, 0, 0);
|
||||
g_stage = "verify-mismatch";
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Existence check without stat() (the sandbox lies about stat sizes). */
|
||||
static int exists(const char *p){
|
||||
FILE *f = fopen(p, "rb");
|
||||
if(!f) return 0;
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* mkdir -p (parents as needed). 0 ok or already there. */
|
||||
static int mkdirs(const char *path){
|
||||
char tmp[256];
|
||||
size_t i, n;
|
||||
if(!path || !path[0]) return -1;
|
||||
n = strlen(path);
|
||||
if(n >= sizeof tmp) return -1;
|
||||
memcpy(tmp, path, n + 1);
|
||||
for(i = 1; i < n; i++){
|
||||
if(tmp[i] == '/'){
|
||||
tmp[i] = 0;
|
||||
mkdir(tmp, 0777);
|
||||
tmp[i] = '/';
|
||||
}
|
||||
}
|
||||
if(mkdir(path, 0777) != 0 && errno != EEXIST) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int step_files(void){
|
||||
char report[512];
|
||||
int ok = -1;
|
||||
int evict_ok = -1;
|
||||
mkdir(INST_DIR, 0777);
|
||||
mkdirs("/data/payloads");
|
||||
/* Evict any running orbisRPC daemon before copying new payload.
|
||||
* kill() is blocked by the sandbox (EPERM), so use
|
||||
* sceKernelLoadStartModule to launch evict.elf as a module.
|
||||
* evict.elf reads daemon.lock, kills the daemon, exits. */
|
||||
{
|
||||
evict_ok = copy_file(EVICT_ELF, EVICT_BIN);
|
||||
ilog("evict-copy", evict_ok, 0, 0);
|
||||
if(evict_ok == 0){
|
||||
uint32_t rv = sceKernelLoadStartModule(EVICT_BIN, 0, NULL, 0, NULL, NULL);
|
||||
ilog("evict-launch", rv, 0, 0);
|
||||
sceKernelUsleep(500000);
|
||||
}
|
||||
}
|
||||
/* Copy daemon payload. */
|
||||
ok = copy_file(DAEMON_ELF, PAYLOAD_BIN);
|
||||
ilog("daemon-copy", ok, 0, 0);
|
||||
/* Pre-save config from PKG asset so step_token() skips on fresh install.
|
||||
* Copy config.json from /app0/assets/config.json to /data/orbisRPC/config.json. */
|
||||
{
|
||||
FILE *src = fopen("/app0/assets/config.json", "rb");
|
||||
if(src){
|
||||
fclose(src);
|
||||
copy_file("/app0/assets/config.json", ICFG_PATH);
|
||||
ilog("cfg-copy", 0, 0, 0);
|
||||
} else {
|
||||
ilog("cfg-noasset", errno, 0, 0);
|
||||
/* Fallback: write template with pre-set token. */
|
||||
FILE *f = fopen(ICFG_PATH, "wb");
|
||||
if(f){
|
||||
fputs("{\"schema_version\":1,\"token\":\"SET_ME\",\"presence_state\":\"On PS4\"}", f);
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
snprintf(report, sizeof report,
|
||||
"%s : %s%s%s%s\n"
|
||||
"%s : %s%s%s%s",
|
||||
PAYLOAD_BIN, ok == 0 ? "OK" : "denied",
|
||||
ok == 0 ? "" : " [stage ", ok == 0 ? "" : g_stage, ok == 0 ? "" : "]",
|
||||
EVICT_BIN, evict_ok == 0 ? "OK" : "denied",
|
||||
evict_ok == 0 ? "" : " [stage ", evict_ok == 0 ? "" : g_stage, evict_ok == 0 ? "" : "]");
|
||||
ui_ok(report);
|
||||
if(ok != 0){
|
||||
ui_ok("Install failed: could not write the payload.\n\nStopping here.");
|
||||
return -1;
|
||||
}
|
||||
ilogv("report-done", ok, 0);
|
||||
/* Config template when missing; never clobbers learned titles. */
|
||||
{
|
||||
char tok[160];
|
||||
tok[0] = 0;
|
||||
icfg_token_load(ICFG_PATH, tok, sizeof tok);
|
||||
if(!token_valid(tok)){
|
||||
FILE *f = fopen(ICFG_PATH, "rb");
|
||||
if(!f){
|
||||
f = fopen(ICFG_PATH, "wb");
|
||||
if(f){
|
||||
fputs("{\"schema_version\":1,\"token\":\"SET_ME\",\"presence_state\":\"On PS4\"}", f);
|
||||
fclose(f);
|
||||
}
|
||||
} else {
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
ilogv("cfg-done", 0, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 2: token. Skips silently when one already validates. */
|
||||
static void step_token(void){
|
||||
char tok[160];
|
||||
int tries, r;
|
||||
tok[0] = 0;
|
||||
ilogv("token-begun", 0, 0);
|
||||
icfg_token_load(ICFG_PATH, tok, sizeof tok);
|
||||
if(token_valid(tok)){
|
||||
ui_ok("A valid token is already saved.\n\nSkipping.");
|
||||
ilogv("token-skip", 0, 0);
|
||||
return;
|
||||
}
|
||||
for(tries = 0; tries < 3; tries++){
|
||||
r = ui_input("Discord token", "paste token, Done to save", tok, sizeof tok);
|
||||
if(r < 0){ ui_ok("Text input failed. Skipping."); return; }
|
||||
if(r == 0) return;
|
||||
if(token_valid(tok)){
|
||||
r = icfg_token_save(ICFG_PATH, tok);
|
||||
ui_ok("Config saved.");
|
||||
return;
|
||||
}
|
||||
ui_ok("That doesn't look like a token.\nCheck it and try again, or cancel to skip.");
|
||||
}
|
||||
}
|
||||
|
||||
/* --- clean exit ---------------------------------------------------
|
||||
* Tear dialogs down, unload modules, then _exit. Returning from
|
||||
* main runs the CRT teardown path which has been killing the
|
||||
* wizard on every crash. */
|
||||
static void finish(int code){
|
||||
ilogv(code == 0 ? "exit" : "fatal", code, 0);
|
||||
ui_shutdown();
|
||||
_exit(code);
|
||||
}
|
||||
|
||||
int main(void){
|
||||
/* Hide the PS4 splash first — dialogs opened while the splash
|
||||
* is visible get auto-dismissed (half-second flash) or never
|
||||
* surface. Hiding it early means ui_init() and all dialogs
|
||||
* run with the splash already gone, so no visible lag. */
|
||||
sceSystemServiceHideSplashScreen();
|
||||
crash_guard();
|
||||
if(ui_init() != 0) finish(1);
|
||||
ui_ok("orbisRPC");
|
||||
if(step_assets() != 0) finish(1);
|
||||
ilogv("assets-ok", 0, 0);
|
||||
if(step_files() != 0) finish(1);
|
||||
step_token();
|
||||
ilogv("post-token", 0, 0);
|
||||
ui_ok("Done.\n\n"
|
||||
"Payloads in /data/payloads.\n"
|
||||
"Launch orbisrpc from the payload launcher.");
|
||||
finish(0);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/* nettest.c - connect-only probes. A closed port and a filtered host
|
||||
* both read as unreachable; that honesty is the point (Sony TMDB from
|
||||
* the console genuinely fails while Discord succeeds). */
|
||||
#include "nettest.h"
|
||||
#include "send.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/select.h>
|
||||
|
||||
/* Non-blocking connect with a hard deadline lives in send.c (shared);
|
||||
* blocking connect ignores SNDTIMEO on filtered hosts (75 s hole). */
|
||||
|
||||
int net_probe(const char *host, int port, int timeout_s){
|
||||
struct addrinfo hints, *res = NULL, *rp;
|
||||
char svc[8];
|
||||
int ok = 0;
|
||||
if(!host || !host[0] || port <= 0 || port > 65535) return 0;
|
||||
if(net_init() != 0) return 0; /* stack dead: everything unreachable */
|
||||
if(timeout_s < 1) timeout_s = 1;
|
||||
if(timeout_s > 10) timeout_s = 10;
|
||||
snprintf(svc, sizeof svc, "%d", port);
|
||||
memset(&hints, 0, sizeof hints);
|
||||
hints.ai_family = AF_INET; /* v4 only: deterministic on console stacks */
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
if(getaddrinfo(host, svc, &hints, &res) != 0 || !res) return 0;
|
||||
for(rp = res; rp; rp = rp->ai_next){
|
||||
int fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
|
||||
if(fd < 0) continue;
|
||||
if(sock_connect_deadline(fd, rp->ai_addr, rp->ai_addrlen, timeout_s) == 0) ok = 1;
|
||||
close(fd);
|
||||
if(ok) break;
|
||||
}
|
||||
freeaddrinfo(res);
|
||||
return ok;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/* nettest.h - honest TCP reachability probes (no traffic beyond connect). */
|
||||
#ifndef INSTALLER_NETTEST_H
|
||||
#define INSTALLER_NETTEST_H
|
||||
|
||||
/* 1 reachable, 0 not. timeout_s clamped 1..10. Host-testable. */
|
||||
int net_probe(const char *host, int port, int timeout_s);
|
||||
|
||||
#endif
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
@@ -0,0 +1,136 @@
|
||||
/* send.c - loopback payload injection: file bytes over TCP to the
|
||||
* console's own loaders. Chunked send (no whole-file malloc). */
|
||||
#include "send.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/select.h>
|
||||
#ifdef INSTALLER_PS4
|
||||
#include <orbis/Net.h>
|
||||
#include <orbis/Sysmodule.h>
|
||||
#endif
|
||||
|
||||
#define SEND_CHUNK (64u*1024u)
|
||||
static const int SEND_PORTS[] = { 9090, 9021, 9020 };
|
||||
|
||||
int net_init(void){
|
||||
#ifdef INSTALLER_PS4
|
||||
static int done = 0;
|
||||
int probe;
|
||||
if(done) return 0;
|
||||
/* Internal NET module (Payload Guest boot pattern) + best-effort
|
||||
* stack init. Ground truth is a probe socket, not return codes. */
|
||||
(void)sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_NET);
|
||||
(void)sceNetInit();
|
||||
(void)sceNetPoolCreate("orbisrpc", 64*1024, 0);
|
||||
probe = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if(probe < 0) return -1;
|
||||
close(probe);
|
||||
done = 1;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sock_connect_deadline(int fd, const struct sockaddr *sa, socklen_t len, int timeout_s){
|
||||
int flags, rc, err = 0;
|
||||
socklen_t elen = sizeof err;
|
||||
fd_set wf;
|
||||
struct timeval tv;
|
||||
if(timeout_s < 1) timeout_s = 1;
|
||||
if(timeout_s > 15) timeout_s = 15;
|
||||
flags = fcntl(fd, F_GETFL, 0);
|
||||
if(flags < 0) return -1;
|
||||
if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) return -1;
|
||||
rc = connect(fd, sa, len);
|
||||
if(rc == 0){
|
||||
fcntl(fd, F_SETFL, flags);
|
||||
return 0;
|
||||
}
|
||||
if(errno != EINPROGRESS){
|
||||
fcntl(fd, F_SETFL, flags);
|
||||
return -1;
|
||||
}
|
||||
FD_ZERO(&wf);
|
||||
FD_SET(fd, &wf);
|
||||
tv.tv_sec = timeout_s;
|
||||
tv.tv_usec = 0;
|
||||
rc = select(fd + 1, NULL, &wf, NULL, &tv);
|
||||
fcntl(fd, F_SETFL, flags);
|
||||
if(rc <= 0) return -1;
|
||||
if(getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &elen) != 0) return -1;
|
||||
return err == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
static int send_one(int fd, const char *path, void (*progress)(unsigned)){
|
||||
FILE *f = fopen(path, "rb");
|
||||
/* Static, not stack: 64 KB is a real fraction of an app thread stack. */
|
||||
static unsigned char buf[SEND_CHUNK];
|
||||
long total = 0, sent = 0;
|
||||
size_t n;
|
||||
if(!f) return -1;
|
||||
if(fseek(f, 0, SEEK_END) != 0){ fclose(f); return -1; }
|
||||
total = ftell(f);
|
||||
if(total <= 0 || total > 16*1024*1024){ fclose(f); return -1; }
|
||||
if(fseek(f, 0, SEEK_SET) != 0){ fclose(f); return -1; }
|
||||
if(progress) progress(0);
|
||||
while((n = fread(buf, 1, sizeof buf, f)) > 0){
|
||||
size_t off = 0;
|
||||
int stalls = 0;
|
||||
while(off < n){
|
||||
ssize_t w = send(fd, buf + off, n - off, 0);
|
||||
if(w <= 0){
|
||||
/* Blocking socket + SNDTIMEO: EAGAIN means the loader
|
||||
* stopped reading. Retry briefly, then fail instead of
|
||||
* spinning forever. */
|
||||
if((errno == EAGAIN || errno == EWOULDBLOCK) && ++stalls < 4)
|
||||
continue;
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
stalls = 0;
|
||||
off += (size_t)w;
|
||||
}
|
||||
sent += (long)n;
|
||||
if(progress) progress((unsigned)(sent * 100 / total));
|
||||
}
|
||||
fclose(f);
|
||||
if(progress) progress(100);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int send_file_loopback(const char *path, int *port_used, void (*progress)(unsigned)){
|
||||
unsigned i;
|
||||
if(!path || !path[0]) return -1;
|
||||
if(net_init() != 0) return -1; /* no stack, no sockets: fail, don't crash */
|
||||
for(i = 0; i < sizeof SEND_PORTS/sizeof SEND_PORTS[0]; i++){
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
struct sockaddr_in sa;
|
||||
struct timeval tv = { 20, 0 };
|
||||
if(fd < 0) continue;
|
||||
memset(&sa, 0, sizeof sa);
|
||||
sa.sin_family = AF_INET;
|
||||
sa.sin_port = htons((uint16_t)SEND_PORTS[i]);
|
||||
sa.sin_addr.s_addr = inet_addr("127.0.0.1");
|
||||
if(sock_connect_deadline(fd, (struct sockaddr *)&sa, sizeof sa, 5) < 0){
|
||||
close(fd);
|
||||
continue;
|
||||
}
|
||||
/* Back to blocking for the bulk send, WITH a send timeout so a
|
||||
* stalled loader surfaces EAGAIN (capped retries below) instead
|
||||
* of wedging forever. */
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||
if(send_one(fd, path, progress) == 0){
|
||||
close(fd);
|
||||
if(port_used) *port_used = SEND_PORTS[i];
|
||||
return 0;
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/* send.h - loopback payload injection (clean-room, standard sockets).
|
||||
* Tries native GoldHEN BinLoader 9090, then elfldr 9021, then 9020.
|
||||
* 9020 is one-shot: accepted bytes count as sent. */
|
||||
#ifndef INSTALLER_SEND_H
|
||||
#define INSTALLER_SEND_H
|
||||
#include <sys/socket.h>
|
||||
|
||||
/* progress(pct) may be NULL. Returns 0 + port_used set on success. */
|
||||
int send_file_loopback(const char *path, int *port_used, void (*progress)(unsigned pct));
|
||||
|
||||
/* Non-blocking connect with a hard deadline (filtered hosts can't stall
|
||||
* past timeout_s). 0 connected, -1 failed. Leaves fd blocking. */
|
||||
int sock_connect_deadline(int fd, const struct sockaddr *sa, socklen_t len, int timeout_s);
|
||||
|
||||
/* One-time network stack init (sceNetInit + pool). 0 ready, -1 dead.
|
||||
* No-op returning 0 on host test builds. Safe to call repeatedly. */
|
||||
int net_init(void);
|
||||
|
||||
#endif
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
/* ui.c - native PS4 dialogs: MsgDialog (ok / yes-no / progress) + ImeDialog.
|
||||
* Everything here needs the console (dialog system calls); pure logic
|
||||
* (validation, config merge) lives in icfg.c and is host-tested. */
|
||||
#include "ui.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <wchar.h>
|
||||
#include <orbis/CommonDialog.h>
|
||||
#include <orbis/MsgDialog.h>
|
||||
#include <orbis/ImeDialog.h>
|
||||
#include <orbis/UserService.h>
|
||||
#include <orbis/Sysmodule.h>
|
||||
#include <orbis/Pad.h>
|
||||
#include <orbis/libkernel.h>
|
||||
#include <orbis/_types/user.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static void ime_dbg(const char *msg){
|
||||
int fd = open("/data/ime_debug.log", O_WRONLY|O_CREAT|O_APPEND, 0666);
|
||||
if(fd >= 0){ write(fd, msg, strlen(msg)); close(fd); }
|
||||
}
|
||||
|
||||
static void base_init(OrbisMsgDialogParam *param){
|
||||
memset(param, 0, sizeof(*param));
|
||||
param->baseParam.size = (uint32_t)sizeof(param->baseParam);
|
||||
param->baseParam.magic =
|
||||
(uint32_t)(ORBIS_COMMON_DIALOG_MAGIC_NUMBER + (uint64_t)¶m->baseParam);
|
||||
param->size = sizeof(OrbisMsgDialogParam);
|
||||
param->mode = ORBIS_MSG_DIALOG_MODE_USER_MSG;
|
||||
}
|
||||
|
||||
static int ui_ready = 0;
|
||||
static int ime_dialog_running = 0;
|
||||
|
||||
/* Reap any stale dialog from a previous session that died
|
||||
* mid-flow. Called by ui_init() to ensure a clean start. */
|
||||
static void reap_stale(void);
|
||||
|
||||
int ui_init(void){
|
||||
if(ui_ready) return 0;
|
||||
/* Reap any stale dialog from a previous session that died
|
||||
* mid-flow. This ensures a clean start. */
|
||||
reap_stale();
|
||||
{
|
||||
/* UserService first: dialogs + pad + IME all key off the user.
|
||||
* Best-effort (already-initialized is fine); uid fallbacks
|
||||
* downstream keep every path forward-safe. */
|
||||
OrbisUserServiceInitializeParams up;
|
||||
memset(&up, 0, sizeof up);
|
||||
up.priority = ORBIS_KERNEL_PRIO_FIFO_LOWEST;
|
||||
(void)sceUserServiceInitialize(&up);
|
||||
}
|
||||
/* Mirrors apollo-ps4 initInternal()/initPad():
|
||||
* 1. internal modules (SYSTEM, USER, COMMON_DIALOG)
|
||||
* 2. sceCommonDialogInitialize() <- BEFORE external modules
|
||||
* 3. PAD module + scePadInit()
|
||||
* 4. external modules (MESSAGE_DIALOG, IME_DIALOG, IME_BACKEND)
|
||||
* Order matters: dialog init must precede external module loads,
|
||||
* and COMMON_DIALOG must precede sceCommonDialogInitialize(). */
|
||||
if(sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_SYSTEM_SERVICE) != 0) return -1;
|
||||
if(sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_USER_SERVICE) != 0) return -1;
|
||||
if(sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_COMMON_DIALOG) != 0) return -1;
|
||||
if(sceCommonDialogInitialize() < 0) return -1;
|
||||
if(sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_PAD) != 0) return -1;
|
||||
(void)scePadInit();
|
||||
if(sceSysmoduleLoadModule(ORBIS_SYSMODULE_MESSAGE_DIALOG) < 0) return -1;
|
||||
if(sceSysmoduleLoadModule(ORBIS_SYSMODULE_IME_DIALOG) < 0){
|
||||
sceSysmoduleUnloadModule(ORBIS_SYSMODULE_MESSAGE_DIALOG);
|
||||
return -1;
|
||||
}
|
||||
if(sceSysmoduleLoadModule(ORBIS_SYSMODULE_IME_BACKEND) < 0){
|
||||
sceSysmoduleUnloadModule(ORBIS_SYSMODULE_IME_DIALOG);
|
||||
sceSysmoduleUnloadModule(ORBIS_SYSMODULE_MESSAGE_DIALOG);
|
||||
return -1;
|
||||
}
|
||||
ui_ready = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* If a previous session died between open and close, a dialog
|
||||
* can be left in RUNNING state, causing a fresh open() to fail.
|
||||
* This function is called by ui_init() to reclaim any stale
|
||||
* dialog before starting fresh. Currently unused (no stale
|
||||
* dialogs observed after ui_init), kept for safety. */
|
||||
static void reap_stale(void){
|
||||
if(sceMsgDialogGetStatus() == ORBIS_COMMON_DIALOG_STATUS_RUNNING)
|
||||
sceMsgDialogTerminate();
|
||||
if(sceImeDialogGetStatus() == ORBIS_DIALOG_STATUS_RUNNING)
|
||||
sceImeDialogTerm();
|
||||
}
|
||||
|
||||
int ui_ok(const char *msg){
|
||||
OrbisMsgDialogParam param;
|
||||
OrbisMsgDialogUserMessageParam um;
|
||||
OrbisMsgDialogResult res;
|
||||
memset(&res, 0, sizeof res);
|
||||
sceMsgDialogTerminate();
|
||||
if(sceMsgDialogInitialize() < 0) return -1;
|
||||
base_init(¶m);
|
||||
memset(&um, 0, sizeof um);
|
||||
um.msg = msg;
|
||||
um.buttonType = ORBIS_MSG_DIALOG_BUTTON_TYPE_OK;
|
||||
param.userMsgParam = &um;
|
||||
if(sceMsgDialogOpen(¶m) < 0){ sceMsgDialogTerminate(); return -1; }
|
||||
/* Yield while waiting: a hot spin starves the dialog service on the
|
||||
* console (the proven pattern on this box sleeps 20 ms per tick). */
|
||||
while(sceMsgDialogUpdateStatus() != ORBIS_COMMON_DIALOG_STATUS_FINISHED)
|
||||
sceKernelUsleep(20000);
|
||||
sceMsgDialogClose();
|
||||
sceMsgDialogGetResult(&res);
|
||||
sceMsgDialogTerminate();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Yes/No dialog. Returns 1 (Yes) or 0 (No/closed).
|
||||
* The dialog uses YESNO_FOCUS_NO which focuses the "No" button.
|
||||
* On this console, Circle=confirm and X=back. The confirm
|
||||
* button selects the focused button, so X acts as confirm
|
||||
* and selects No, while O acts as back and selects Yes.
|
||||
* Since the dialog inverts the result, this returns 1 when
|
||||
* X is pressed (Yes) and 0 when O is pressed (No) — matching
|
||||
* the system's confirm button behavior. Kept for future use. */
|
||||
int ui_confirm(const char *msg){
|
||||
OrbisMsgDialogParam param;
|
||||
OrbisMsgDialogUserMessageParam um;
|
||||
OrbisMsgDialogResult res;
|
||||
memset(&res, 0, sizeof res);
|
||||
sceMsgDialogTerminate();
|
||||
if(sceMsgDialogInitialize() < 0) return -1;
|
||||
base_init(¶m);
|
||||
memset(&um, 0, sizeof um);
|
||||
um.msg = msg;
|
||||
/* YESNO_FOCUS_NO focuses the "No" button. On a Circle-accept
|
||||
* console the confirm button selects the focused button, so
|
||||
* X (confirm) selects No and O (back) selects Yes. The
|
||||
* result is inverted: X→buttonId=NO→returns 1 (Yes). */
|
||||
um.buttonType = ORBIS_MSG_DIALOG_BUTTON_TYPE_YESNO_FOCUS_NO;
|
||||
param.userMsgParam = &um;
|
||||
if(sceMsgDialogOpen(¶m) < 0){ sceMsgDialogTerminate(); return -1; }
|
||||
while(sceMsgDialogUpdateStatus() != ORBIS_COMMON_DIALOG_STATUS_FINISHED)
|
||||
sceKernelUsleep(20000);
|
||||
sceMsgDialogClose();
|
||||
sceMsgDialogGetResult(&res);
|
||||
sceMsgDialogTerminate();
|
||||
return res.buttonId == ORBIS_MSG_DIALOG_BUTTON_ID_YES;
|
||||
}
|
||||
|
||||
static int progress_open = 0;
|
||||
|
||||
int ui_progress_open(const char *msg){
|
||||
OrbisMsgDialogParam param;
|
||||
OrbisMsgDialogProgressBarParam bar;
|
||||
if(progress_open){
|
||||
/* Self-healing: a prior session that died between open and close
|
||||
* leaves the flag set with no dialog behind it. Reclaim it. */
|
||||
ui_progress_close();
|
||||
}
|
||||
sceMsgDialogTerminate();
|
||||
if(sceMsgDialogInitialize() < 0) return -1;
|
||||
base_init(¶m);
|
||||
param.mode = ORBIS_MSG_DIALOG_MODE_PROGRESS_BAR;
|
||||
memset(&bar, 0, sizeof bar);
|
||||
bar.barType = ORBIS_MSG_DIALOG_PROGRESSBAR_TYPE_PERCENTAGE;
|
||||
bar.msg = msg;
|
||||
param.progBarParam = &bar;
|
||||
if(sceMsgDialogOpen(¶m) < 0){ sceMsgDialogTerminate(); return -1; }
|
||||
progress_open = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ui_progress_msg(const char *msg){
|
||||
if(progress_open && msg) sceMsgDialogProgressBarSetMsg(0, msg);
|
||||
}
|
||||
|
||||
void ui_progress_set(unsigned pct){
|
||||
if(progress_open) sceMsgDialogProgressBarSetValue(0, pct > 100 ? 100 : pct);
|
||||
}
|
||||
|
||||
void ui_progress_close(void){
|
||||
if(!progress_open) return;
|
||||
progress_open = 0;
|
||||
sceMsgDialogClose();
|
||||
sceMsgDialogTerminate();
|
||||
}
|
||||
|
||||
int ui_input(const char *title, const char *placeholder, char *out, size_t cap){
|
||||
static wchar_t wbuf[256];
|
||||
static wchar_t wtitle[64];
|
||||
static wchar_t wplace[64];
|
||||
OrbisImeDialogSetting st;
|
||||
int32_t uid = 0;
|
||||
size_t i;
|
||||
if(!out || cap == 0 || cap > 200) return -1;
|
||||
/* Prefill with current value (ASCII round-trip). */
|
||||
for(i = 0; i < cap - 1 && out[i]; i++) wbuf[i] = (wchar_t)(unsigned char)out[i];
|
||||
wbuf[i] = 0;
|
||||
for(i = 0; i < 63 && title && title[i]; i++) wtitle[i] = (wchar_t)(unsigned char)title[i];
|
||||
wtitle[i] = 0;
|
||||
for(i = 0; i < 63 && placeholder && placeholder[i]; i++) wplace[i] = (wchar_t)(unsigned char)placeholder[i];
|
||||
wplace[i] = 0;
|
||||
if(sceUserServiceGetForegroundUser(&uid) < 0 || uid <= 0) uid = 1;
|
||||
memset(&st, 0, sizeof st);
|
||||
st.userId = (uint32_t)uid;
|
||||
st.type = 0;
|
||||
st.supportedLanguages = 0;
|
||||
st.enterLabel = ORBIS_BUTTON_LABEL_DEFAULT;
|
||||
st.inputMethod = 0;
|
||||
st.filter = 0;
|
||||
st.option = 0;
|
||||
st.maxTextLength = (uint32_t)(cap - 1);
|
||||
st.inputTextBuffer = wbuf;
|
||||
/* Center of 1920x1080. At (0,0) the panel anchors up-left and the
|
||||
* keyboard renders half off-screen; 960,540 is the layout that is
|
||||
* known-good on this console (rutracker-ps4 uses the same values). */
|
||||
st.posx = 960;
|
||||
st.posy = 540;
|
||||
st.horizontalAlignment = ORBIS_H_CENTER;
|
||||
st.verticalAlignment = ORBIS_V_CENTER;
|
||||
st.placeholder = wplace;
|
||||
st.title = wtitle;
|
||||
/* apollo-ps4 pattern: guard against re-entry, terminate stale
|
||||
* state, then init. The dialog owns the running flag. */
|
||||
if(ime_dialog_running){ sceImeDialogTerm(); ime_dialog_running = 0; }
|
||||
ime_dbg("ime_begin\n");
|
||||
if(sceImeDialogInit(&st, NULL) < 0){ ime_dbg("ime_init_fail\n"); return -1; }
|
||||
ime_dialog_running = 1;
|
||||
{
|
||||
int spins = 0;
|
||||
OrbisDialogStatus ds;
|
||||
ds = sceImeDialogGetStatus();
|
||||
if(ds == ORBIS_DIALOG_STATUS_RUNNING) ime_dbg("ime_status_running\n");
|
||||
else if(ds == ORBIS_DIALOG_STATUS_NONE) ime_dbg("ime_status_none\n");
|
||||
else if(ds == ORBIS_DIALOG_STATUS_STOPPED) ime_dbg("ime_status_stopped\n");
|
||||
while(ime_dialog_running){
|
||||
ds = sceImeDialogGetStatus();
|
||||
if(ds == ORBIS_DIALOG_STATUS_STOPPED){
|
||||
OrbisDialogResult res;
|
||||
memset(&res, 0, sizeof res);
|
||||
sceImeDialogGetResult(&res);
|
||||
sceImeDialogTerm();
|
||||
ime_dialog_running = 0;
|
||||
if(res.endstatus != ORBIS_DIALOG_OK) return 0;
|
||||
break;
|
||||
}
|
||||
if(ds == ORBIS_DIALOG_STATUS_NONE && ++spins > 250){
|
||||
ime_dbg("ime_timeout\n");
|
||||
sceImeDialogTerm();
|
||||
ime_dialog_running = 0;
|
||||
return -1;
|
||||
}
|
||||
if(ds != ORBIS_DIALOG_STATUS_NONE) spins = 0;
|
||||
sceKernelUsleep(20000);
|
||||
}
|
||||
}
|
||||
for(i = 0; i < cap - 1 && wbuf[i]; i++)
|
||||
out[i] = (wbuf[i] < 128 && wbuf[i] >= 32) ? (char)wbuf[i] : '?';
|
||||
out[i] = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Teardown for exit: terminate any live dialog, then unload the
|
||||
* modules loaded in ui_init. Called before _exit so no dialog
|
||||
* outlives the app. Must unload PAD too since it was loaded
|
||||
* internally (ORBIS_SYSMODULE_INTERNAL_PAD). */
|
||||
void ui_shutdown(void){
|
||||
if(!ui_ready) return;
|
||||
ui_ready = 0;
|
||||
progress_open = 0;
|
||||
ime_dialog_running = 0;
|
||||
sceMsgDialogTerminate();
|
||||
sceImeDialogTerm();
|
||||
sceSysmoduleUnloadModule(ORBIS_SYSMODULE_IME_BACKEND);
|
||||
sceSysmoduleUnloadModule(ORBIS_SYSMODULE_IME_DIALOG);
|
||||
sceSysmoduleUnloadModule(ORBIS_SYSMODULE_MESSAGE_DIALOG);
|
||||
sceSysmoduleUnloadModule(ORBIS_SYSMODULE_INTERNAL_PAD);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/* ui.h - native dialog helpers for the installer wizard. */
|
||||
#ifndef INSTALLER_UI_H
|
||||
#define INSTALLER_UI_H
|
||||
#include <stddef.h>
|
||||
|
||||
/* Must run once before any dialog: loads MsgDialog/ImeDialog sysmodules
|
||||
* and inits CommonDialog. Calling dialogs without this panics the box
|
||||
* (unloaded-sysmodule call at startup). 0 ok, -1 fatal. */
|
||||
int ui_init(void);
|
||||
|
||||
/* Info dialog with OK. 0 shown, -1 failed to open. */
|
||||
int ui_ok(const char *msg);
|
||||
/* Yes/No dialog: X = enter (Yes), O = back (No). 1 yes, 0 no/closed,
|
||||
* -1 error. */
|
||||
int ui_confirm(const char *msg);
|
||||
|
||||
/* Progress dialog. Open once, update, close. 0 ok, -1 error. */
|
||||
int ui_progress_open(const char *msg);
|
||||
void ui_progress_msg(const char *msg);
|
||||
void ui_progress_set(unsigned pct);
|
||||
void ui_progress_close(void);
|
||||
|
||||
/* OSK text input. Prefills with out's current content. Returns 1 + out
|
||||
* filled on Done, 0 on cancel, -1 on error. */
|
||||
int ui_input(const char *title, const char *placeholder, char *out, size_t cap);
|
||||
|
||||
/* Terminate dialogs + unload their sysmodules. Call once before exit. */
|
||||
void ui_shutdown(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,147 @@
|
||||
/* appdb.c - display names from /system_data/priv/mms/app.db (read-only).
|
||||
* Method follows the proven on-console pattern (SonicLoader et al.): SQLite
|
||||
* query, never raw byte-scanning (an earlier revision byte-scanned app.db
|
||||
* and returned wrong names: the file reads fine, the parse was garbage).
|
||||
* Tiers, first hit wins:
|
||||
* 1. tbl_appbrowse.titleName (when the firmware carries it)
|
||||
* 2. tbl_appinfo val for key TITLE / TITLE_xx (localized display titles)
|
||||
* 3. /user/appmeta/<id>/param.json title fields (ShadowMountPlus pattern)
|
||||
* Every tier is schema-guarded and fail-soft: missing tables, missing keys,
|
||||
* unreadable files, and locked DBs all fall through to the next source.
|
||||
* Host-testable via appdb_title_from() with a synthetic DB path. */
|
||||
#include "appdb.h"
|
||||
#include "jsonlite.h"
|
||||
#include "log.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include "sqlite3.h"
|
||||
|
||||
#define APPDB_PATH "/system_data/priv/mms/app.db"
|
||||
#define APPMETA_BASE "/user/appmeta"
|
||||
|
||||
static int valid_tid(const char *t){
|
||||
if(!t) return 0;
|
||||
size_t n = strlen(t);
|
||||
if(n != 9) return 0;
|
||||
for(size_t i = 0; i < n; i++){
|
||||
char c = t[i];
|
||||
if(!((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int table_exists(sqlite3 *db, const char *name){
|
||||
sqlite3_stmt *st = NULL;
|
||||
int found = 0;
|
||||
if(sqlite3_prepare_v2(db, "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?1 LIMIT 1;",
|
||||
-1, &st, NULL) != SQLITE_OK)
|
||||
return 0;
|
||||
sqlite3_bind_text(st, 1, name, -1, SQLITE_TRANSIENT);
|
||||
if(sqlite3_step(st) == SQLITE_ROW) found = 1;
|
||||
sqlite3_finalize(st);
|
||||
return found;
|
||||
}
|
||||
|
||||
/* Copy first non-empty TEXT result of a one-row query. 0 on hit. */
|
||||
static int query_name(sqlite3 *db, const char *sql, const char *titleId,
|
||||
char *out, size_t cap){
|
||||
sqlite3_stmt *st = NULL;
|
||||
int rc = -1;
|
||||
if(sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
|
||||
sqlite3_bind_text(st, 1, titleId, -1, SQLITE_TRANSIENT);
|
||||
if(sqlite3_step(st) == SQLITE_ROW){
|
||||
const unsigned char *v = sqlite3_column_text(st, 0);
|
||||
if(v && v[0]){
|
||||
strncpy(out, (const char *)v, cap - 1);
|
||||
out[cap - 1] = 0;
|
||||
if(out[0] && strlen(out) > 1) rc = 0;
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* param.json title fields: small single read, jsonlite parse. Best-effort. */
|
||||
static int paramjson_title(const char *appmeta_base, const char *titleId,
|
||||
char *out, size_t cap){
|
||||
char path[256];
|
||||
int n = snprintf(path, sizeof path, "%s/%s/param.json",
|
||||
appmeta_base ? appmeta_base : APPMETA_BASE, titleId);
|
||||
if(n <= 0 || (size_t)n >= sizeof path) return -1;
|
||||
int fd = open(path, O_RDONLY);
|
||||
if(fd < 0) return -1;
|
||||
char buf[4096];
|
||||
ssize_t r = read(fd, buf, sizeof buf - 1);
|
||||
close(fd);
|
||||
if(r <= 0) return -1;
|
||||
buf[r] = 0;
|
||||
jl_val_t *root = jl_parse(buf, (size_t)r);
|
||||
if(!root) return -1;
|
||||
static const char *keys[] = { "titleName", "title", "name", NULL };
|
||||
int rc = -1;
|
||||
for(int i = 0; keys[i]; i++){
|
||||
const jl_val_t *v = jl_obj_get(root, keys[i]);
|
||||
if(v && v->type == JL_STRING && v->str && strlen(v->str) > 1){
|
||||
strncpy(out, v->str, cap - 1);
|
||||
out[cap - 1] = 0;
|
||||
rc = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
jl_free(root);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int appdb_title_from(const char *dbpath, const char *appmeta_base,
|
||||
const char *titleId, char *out, size_t cap){
|
||||
if(!out || cap == 0) return -1;
|
||||
out[0] = 0;
|
||||
if(!valid_tid(titleId)) return -1;
|
||||
if(!dbpath) dbpath = APPDB_PATH;
|
||||
|
||||
sqlite3 *db = NULL;
|
||||
int orc = sqlite3_open_v2(dbpath, &db,
|
||||
SQLITE_OPEN_READONLY | SQLITE_OPEN_NOMUTEX, NULL);
|
||||
if(orc != SQLITE_OK){
|
||||
if(db) sqlite3_close(db);
|
||||
return -1;
|
||||
}
|
||||
sqlite3_busy_timeout(db, 2000);
|
||||
|
||||
/* Tier 1: friendly browse name. */
|
||||
if(table_exists(db, "tbl_appbrowse")){
|
||||
if(query_name(db, "SELECT titleName FROM tbl_appbrowse WHERE titleId=?1 "
|
||||
"AND titleName IS NOT NULL AND titleName<>'' LIMIT 1;",
|
||||
titleId, out, cap) == 0){
|
||||
log_msg("name: %s via appdb-browse", out);
|
||||
sqlite3_close(db);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
/* Tier 2: localized TITLE keys; ORDER BY key prefers bare TITLE
|
||||
* (system language) over TITLE_00..NN variants. */
|
||||
if(table_exists(db, "tbl_appinfo")){
|
||||
if(query_name(db, "SELECT val FROM tbl_appinfo WHERE titleId=?1 "
|
||||
"AND (key='TITLE' OR key LIKE 'TITLE\\_%' ESCAPE '\\') "
|
||||
"AND val IS NOT NULL AND val<>'' ORDER BY key LIMIT 1;",
|
||||
titleId, out, cap) == 0){
|
||||
log_msg("name: %s via appdb-info", out);
|
||||
sqlite3_close(db);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
sqlite3_close(db);
|
||||
|
||||
/* Tier 3: staged appmeta descriptor (no DB needed). */
|
||||
if(paramjson_title(appmeta_base, titleId, out, cap) == 0){
|
||||
log_msg("name: %s via paramjson", out);
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int appdb_title(const char *titleId, char *out, size_t cap){
|
||||
return appdb_title_from(NULL, NULL, titleId, out, cap);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/* appdb.h - display names from the system app.db (read-only SQLite). */
|
||||
#ifndef APPDB_H
|
||||
#define APPDB_H
|
||||
#include <stddef.h>
|
||||
/* Resolve a display name for a 9-char title ID. 0 on success. */
|
||||
int appdb_title(const char *titleId, char *out, size_t cap);
|
||||
/* Test seam: explicit DB + appmeta paths (NULL = on-console defaults). */
|
||||
int appdb_title_from(const char *dbpath, const char *appmeta_base,
|
||||
const char *titleId, char *out, size_t cap);
|
||||
#endif
|
||||
+312
@@ -0,0 +1,312 @@
|
||||
/* art.c - external-asset resolution (host-testable parsing + PS4 HTTPS).
|
||||
* Pure parsing lives in art_parse_mp() (unit-tested); the POST itself
|
||||
* mirrors updater.c's bounded HTTPS client. */
|
||||
#include "art.h"
|
||||
#include "jsonlite.h"
|
||||
#include "log.h"
|
||||
#include "updater_http.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* Extract external_asset_path for url from an external-assets response.
|
||||
* Returns 1 + out ("mp:"+path), else 0. Pure function, host-tested. */
|
||||
int art_parse_mp(const char *body, size_t len, const char *url,
|
||||
char *out, size_t cap){
|
||||
if(!body || !url || !out || cap < 8) return 0;
|
||||
jl_val_t *r = jl_parse(body, len);
|
||||
if(!r || r->type != JL_ARRAY){ if(r) jl_free(r); return 0; }
|
||||
int rc = 0;
|
||||
for(size_t i = 0; ; i++){
|
||||
const jl_val_t *e = jl_arr_at(r, i);
|
||||
if(!e) break;
|
||||
if(e->type != JL_OBJECT) continue;
|
||||
const jl_val_t *u = jl_obj_get(e, "url");
|
||||
const jl_val_t *p = jl_obj_get(e, "external_asset_path");
|
||||
if(!u || u->type != JL_STRING || !u->str) continue;
|
||||
if(!p || p->type != JL_STRING || !p->str) continue;
|
||||
if(strcmp(u->str, url) != 0) continue;
|
||||
if(strlen(p->str) > cap - 5) continue;
|
||||
snprintf(out, cap, "mp:%s", p->str);
|
||||
rc = 1;
|
||||
break;
|
||||
}
|
||||
jl_free(r);
|
||||
return rc;
|
||||
}
|
||||
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
#include "tls.h"
|
||||
#include "clock.h"
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#elif defined(__PS4__)
|
||||
#include "tls.h"
|
||||
#include "clock.h"
|
||||
#include <orbis/Net.h>
|
||||
#include <orbis/Sysmodule.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#define ART_HOST "discord.com"
|
||||
#define ART_DEADLINE_S 12
|
||||
#define ART_RESP_MAX (16u*1024u)
|
||||
|
||||
static char s_last_url[512] = "";
|
||||
static char s_last_mp[512] = "";
|
||||
|
||||
void art_cache_clear(void){
|
||||
s_last_url[0] = 0;
|
||||
s_last_mp[0] = 0;
|
||||
}
|
||||
|
||||
/* Disk cache: url -> mp path, 7-day TTL. Survives reboots so titles
|
||||
* resolved once never pay the external-assets round trip again.
|
||||
* Best-effort only: any I/O failure degrades to memory-only caching. */
|
||||
#define ART_DISK_MAX 32
|
||||
#define ART_DISK_TTL (7*24*3600)
|
||||
static struct { char url[512]; char mp[512]; int64_t at; } s_disk[ART_DISK_MAX];
|
||||
static int s_disk_n = -1; /* -1 = not loaded yet */
|
||||
|
||||
static void art_disk_load(void){
|
||||
if(s_disk_n >= 0) return;
|
||||
s_disk_n = 0;
|
||||
FILE *f = fopen("/data/orbisRPC/artwork_cache.json", "rb");
|
||||
if(!f) return;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long sz = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
int64_t now = (int64_t)time(NULL);
|
||||
if(sz > 0 && sz < 65536){
|
||||
char *buf = (char*)malloc((size_t)sz + 1);
|
||||
if(buf && fread(buf, 1, (size_t)sz, f) == (size_t)sz){
|
||||
buf[sz] = 0;
|
||||
jl_val_t *r = jl_parse(buf, (size_t)sz);
|
||||
if(r && r->type == JL_ARRAY){
|
||||
for(size_t i = 0; ; i++){
|
||||
const jl_val_t *e = jl_arr_at(r, i);
|
||||
if(!e) break;
|
||||
if(s_disk_n >= ART_DISK_MAX) break;
|
||||
if(e->type != JL_OBJECT) continue;
|
||||
const jl_val_t *u = jl_obj_get(e, "url");
|
||||
const jl_val_t *m = jl_obj_get(e, "mp");
|
||||
const jl_val_t *a = jl_obj_get(e, "at");
|
||||
if(!u || u->type != JL_STRING || !u->str) continue;
|
||||
if(!m || m->type != JL_STRING || !m->str) continue;
|
||||
if(!a || a->type != JL_NUMBER) continue;
|
||||
if(now - (int64_t)a->num > ART_DISK_TTL) continue; /* prune */
|
||||
snprintf(s_disk[s_disk_n].url, sizeof s_disk[s_disk_n].url, "%s", u->str);
|
||||
snprintf(s_disk[s_disk_n].mp, sizeof s_disk[s_disk_n].mp, "%s", m->str);
|
||||
s_disk[s_disk_n].at = (int64_t)a->num;
|
||||
s_disk_n++;
|
||||
}
|
||||
}
|
||||
if(r) jl_free(r);
|
||||
}
|
||||
free(buf);
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static void art_disk_save(void){
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof tmp, "%s.new", "/data/orbisRPC/artwork_cache.json");
|
||||
FILE *f = fopen(tmp, "wb");
|
||||
if(!f) return;
|
||||
int ok = 1;
|
||||
if(fputs("[", f) < 0) ok = 0;
|
||||
for(int i = 0; ok && i < s_disk_n; i++){
|
||||
if(i > 0 && fputs(",", f) < 0){ ok = 0; break; }
|
||||
/* URLs/mp paths are response-derived; escape defensively. */
|
||||
char eu[1024], em[1024];
|
||||
size_t a = 0, b = 0;
|
||||
for(const char *p = s_disk[i].url; *p && a + 2 < sizeof eu; p++){
|
||||
if(*p == '"' || *p == '\\') eu[a++] = '\\';
|
||||
eu[a++] = *p;
|
||||
}
|
||||
eu[a] = 0;
|
||||
for(const char *p = s_disk[i].mp; *p && b + 2 < sizeof em; p++){
|
||||
if(*p == '"' || *p == '\\') em[b++] = '\\';
|
||||
em[b++] = *p;
|
||||
}
|
||||
em[b] = 0;
|
||||
if(fprintf(f, "{\"url\":\"%s\",\"mp\":\"%s\",\"at\":%lld}",
|
||||
eu, em, (long long)s_disk[i].at) < 0) ok = 0;
|
||||
}
|
||||
if(ok && fputs("]", f) < 0) ok = 0;
|
||||
if(ok && fflush(f) != 0) ok = 0;
|
||||
if(ok){ int fd = fileno(f); if(fd >= 0 && fsync(fd) != 0) ok = 0; }
|
||||
if(fclose(f) != 0) ok = 0;
|
||||
if(ok) rename(tmp, "/data/orbisRPC/artwork_cache.json");
|
||||
else remove(tmp);
|
||||
}
|
||||
|
||||
static int art_disk_get(const char *url, char *out_mp, size_t cap){
|
||||
art_disk_load();
|
||||
int64_t now = (int64_t)time(NULL);
|
||||
for(int i = 0; i < s_disk_n; i++){
|
||||
if(!strcmp(s_disk[i].url, url)){
|
||||
if(now - s_disk[i].at > ART_DISK_TTL) return 0;
|
||||
strncpy(out_mp, s_disk[i].mp, cap - 1);
|
||||
out_mp[cap - 1] = 0;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void art_disk_put(const char *url, const char *mp){
|
||||
art_disk_load();
|
||||
for(int i = 0; i < s_disk_n; i++){
|
||||
if(!strcmp(s_disk[i].url, url)){
|
||||
snprintf(s_disk[i].mp, sizeof s_disk[i].mp, "%s", mp);
|
||||
s_disk[i].at = (int64_t)time(NULL);
|
||||
art_disk_save();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(s_disk_n < ART_DISK_MAX){
|
||||
snprintf(s_disk[s_disk_n].url, sizeof s_disk[s_disk_n].url, "%s", url);
|
||||
snprintf(s_disk[s_disk_n].mp, sizeof s_disk[s_disk_n].mp, "%s", mp);
|
||||
s_disk[s_disk_n].at = (int64_t)time(NULL);
|
||||
s_disk_n++;
|
||||
art_disk_save();
|
||||
}
|
||||
}
|
||||
|
||||
static int art_post(const char *app_id, const char *token, const char *url,
|
||||
char *out_body, size_t body_cap, size_t *out_len){
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
struct addrinfo hints, *res = NULL;
|
||||
memset(&hints, 0, sizeof hints);
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
if(getaddrinfo(ART_HOST, "443", &hints, &res) != 0 || !res) return -1;
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if(fd < 0){ freeaddrinfo(res); return -1; }
|
||||
struct timeval tv = { ART_DEADLINE_S, 0 };
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
||||
if(connect(fd, res->ai_addr, res->ai_addrlen) < 0){
|
||||
close(fd); freeaddrinfo(res); return -1;
|
||||
}
|
||||
freeaddrinfo(res);
|
||||
{
|
||||
int fl = fcntl(fd, F_GETFL, 0);
|
||||
if(fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
||||
}
|
||||
#else
|
||||
/* Host test build: no transport here (parse path still testable). */
|
||||
(void)app_id; (void)token; (void)url;
|
||||
(void)out_body; (void)body_cap; (void)out_len;
|
||||
return -1;
|
||||
#endif
|
||||
#if defined(ORBISRPC_SDK_PAYLOAD) || defined(__PS4__)
|
||||
tls_ctx_t *t = tls_start(fd, ART_HOST);
|
||||
if(!t){
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
close(fd);
|
||||
#else
|
||||
sceNetSocketClose(fd);
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
char payload[768];
|
||||
int pl = snprintf(payload, sizeof payload, "{\"urls\":[\"%s\"]}", url);
|
||||
if(pl <= 0 || pl >= (int)sizeof payload){ tls_free(t); return -1; }
|
||||
char req[1024];
|
||||
int rl = snprintf(req, sizeof req,
|
||||
"POST /api/v10/applications/%s/external-assets HTTP/1.1\r\n"
|
||||
"Host: %s\r\nAuthorization: %s\r\nContent-Type: application/json\r\n"
|
||||
"Content-Length: %d\r\nConnection: close\r\n\r\n",
|
||||
app_id, ART_HOST, token, pl);
|
||||
if(rl <= 0 || rl >= (int)sizeof req){ tls_free(t); return -1; }
|
||||
if(tls_write(t, req, (size_t)rl) < 0){ tls_free(t); return -1; }
|
||||
if(tls_write(t, payload, (size_t)pl) < 0){ tls_free(t); return -1; }
|
||||
size_t bl = 0;
|
||||
int64_t dl = orbis_mono_s() + ART_DEADLINE_S + 10;
|
||||
for(;;){
|
||||
char tmp[1024];
|
||||
int r = tls_read(t, tmp, sizeof tmp - 1);
|
||||
if(r < 0) break;
|
||||
if(r == 0){
|
||||
if(orbis_mono_s() > dl) break;
|
||||
usleep(20000);
|
||||
continue;
|
||||
}
|
||||
if(bl + (size_t)r >= body_cap) break;
|
||||
memcpy(out_body + bl, tmp, (size_t)r);
|
||||
bl += (size_t)r;
|
||||
if(orbis_mono_s() > dl) break;
|
||||
}
|
||||
tls_free(t);
|
||||
if(bl == 0){ return -1; }
|
||||
out_body[bl] = 0;
|
||||
/* Parse with the shared chunked-aware HTTP parser (Discord serves
|
||||
* Transfer-Encoding: chunked here; naive header-splitting reads the
|
||||
* chunk framing as body and the mp: lookup silently misses). */
|
||||
{
|
||||
int st = 0;
|
||||
size_t olen = 0;
|
||||
char *body = upd_parse_response(out_body, bl, body_cap,
|
||||
&st, &olen, NULL, 0);
|
||||
if(!body || st != 200){ free(body); return -1; }
|
||||
if(out_len) *out_len = olen;
|
||||
memmove(out_body, body, olen + 1);
|
||||
free(body);
|
||||
}
|
||||
return 0;
|
||||
#else
|
||||
/* Host test build: no transport; parse path still testable. */
|
||||
(void)app_id; (void)token; (void)url;
|
||||
(void)out_body; (void)body_cap; (void)out_len;
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
int art_resolve_mp(const char *app_id, const char *token, const char *url,
|
||||
char *out_mp, size_t cap){
|
||||
if(!app_id || !app_id[0] || !token || !token[0] || !url || !url[0] ||
|
||||
!out_mp || cap < 8)
|
||||
return 0;
|
||||
if(!strcmp(url, s_last_url) && s_last_mp[0]){
|
||||
strncpy(out_mp, s_last_mp, cap - 1);
|
||||
out_mp[cap - 1] = 0;
|
||||
return 1;
|
||||
}
|
||||
/* Disk cache (7-day TTL): titles resolved on earlier boots skip the
|
||||
* round trip entirely. */
|
||||
if(art_disk_get(url, out_mp, cap)){
|
||||
snprintf(s_last_url, sizeof s_last_url, "%s", url);
|
||||
snprintf(s_last_mp, sizeof s_last_mp, "%s", out_mp);
|
||||
log_msg("art: mp from disk cache");
|
||||
return 1;
|
||||
}
|
||||
static char resp[ART_RESP_MAX];
|
||||
size_t rlen = 0;
|
||||
if(art_post(app_id, token, url, resp, sizeof resp, &rlen) != 0){
|
||||
log_msg("art: external-assets POST failed");
|
||||
return 0;
|
||||
}
|
||||
if(!art_parse_mp(resp, rlen, url, out_mp, cap)){
|
||||
log_msg("art: no mp path for url (resp %zuB: %.120s)", rlen, resp);
|
||||
return 0;
|
||||
}
|
||||
snprintf(s_last_url, sizeof s_last_url, "%s", url);
|
||||
snprintf(s_last_mp, sizeof s_last_mp, "%s", out_mp);
|
||||
art_disk_put(url, out_mp);
|
||||
log_msg("art: resolved mp (%zuB)", strlen(out_mp));
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/* art.h - Discord external-asset (mp:) resolution for artwork.
|
||||
*
|
||||
* Verified on hardware 2026-09-23: raw https URLs and dangling asset keys
|
||||
* in large_image either drop the whole activity or render "?". The working
|
||||
* path used by every functional tool: POST the image URL to
|
||||
* /applications/{app}/external-assets, use the returned mp: path.
|
||||
* Results cached per boot (one call per title). */
|
||||
#ifndef ORBISRPC_ART_H
|
||||
#define ORBISRPC_ART_H
|
||||
#include <stddef.h>
|
||||
/* Resolve url -> mp: path. Returns 1 and fills out_mp ("mp:..." NUL) on
|
||||
* success, 0 when unusable (caller sends no art). Not thread-safe. */
|
||||
int art_resolve_mp(const char *app_id, const char *token, const char *url,
|
||||
char *out_mp, size_t cap);
|
||||
/* Drop the mp: cache (call on new game sessions: mappings can expire and
|
||||
* each title deserves a fresh resolve anyway). */
|
||||
void art_cache_clear(void);
|
||||
/* Pure helper (host-tested): extract mp path for url from response. */
|
||||
int art_parse_mp(const char *body, size_t len, const char *url,
|
||||
char *out, size_t cap);
|
||||
#endif
|
||||
@@ -0,0 +1,17 @@
|
||||
/* b64.c - standard base64 encoder */
|
||||
#include "b64.h"
|
||||
static const char b64t[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
size_t b64_encode(const unsigned char *in, size_t len, char *out){
|
||||
size_t o=0, i;
|
||||
if(!out || (len && !in)) return 0;
|
||||
for(i=0;i<len;i+=3){
|
||||
unsigned char b0=in[i], b1=(i+1<len)?in[i+1]:0, b2=(i+2<len)?in[i+2]:0;
|
||||
unsigned v=((unsigned)b0<<16)|((unsigned)b1<<8)|b2;
|
||||
out[o++]=b64t[(v>>18)&0x3f];
|
||||
out[o++]=b64t[(v>>12)&0x3f];
|
||||
out[o++]=i+1<len?b64t[(v>>6)&0x3f]:'=';
|
||||
out[o++]=i+2<len?b64t[v&0x3f]:'=';
|
||||
}
|
||||
out[o]=0; /* NUL-terminate so callers can use it as a C string */
|
||||
return o;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/* b64.h - standard RFC 4648 Base64 encoder. */
|
||||
#ifndef B64_H
|
||||
#define B64_H
|
||||
#include <stddef.h>
|
||||
size_t b64_encode(const unsigned char *in, size_t len, char *out);
|
||||
/* Writes a NUL-terminated string to out and returns its encoded length.
|
||||
* The caller must provide at least 4 * ((len + 2) / 3) + 1 bytes. */
|
||||
#endif
|
||||
@@ -0,0 +1,500 @@
|
||||
/* ca_bundle_pem.h - curated trust anchors (generated, do not edit).
|
||||
* 22 roots for GitHub/Discord/Sony. Regenerate with scripts/make_ca_bundle.py.
|
||||
*/
|
||||
#ifndef ORBISRPC_CA_BUNDLE_PEM_H
|
||||
#define ORBISRPC_CA_BUNDLE_PEM_H
|
||||
static const char ORBISRPC_CA_BUNDLE_PEM[] =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs\n"
|
||||
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"
|
||||
"d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j\n"
|
||||
"ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL\n"
|
||||
"MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3\n"
|
||||
"LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug\n"
|
||||
"RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm\n"
|
||||
"+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW\n"
|
||||
"PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM\n"
|
||||
"xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB\n"
|
||||
"Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3\n"
|
||||
"hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg\n"
|
||||
"EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF\n"
|
||||
"MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA\n"
|
||||
"FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec\n"
|
||||
"nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z\n"
|
||||
"eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF\n"
|
||||
"hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2\n"
|
||||
"Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe\n"
|
||||
"vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep\n"
|
||||
"+OkuE6N36B9K\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIICCjCCAZGgAwIBAgIQbkepyIuUtui7OyrYorLBmTAKBggqhkjOPQQDAzBHMQsw\n"
|
||||
"CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU\n"
|
||||
"MBIGA1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw\n"
|
||||
"MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp\n"
|
||||
"Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQA\n"
|
||||
"IgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzu\n"
|
||||
"hXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/l\n"
|
||||
"xKvRHYqjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud\n"
|
||||
"DgQWBBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNnADBkAjBqUFJ0\n"
|
||||
"CMRw3J5QdCHojXohw0+WbhXRIjVhLfoIN+4Zba3bssx9BzT1YBkstTTZbyACMANx\n"
|
||||
"sbqjYAuG7ZoIapVon+Kz4ZNkfF6Tpt95LY2F45TPI11xzPKwTdb+mciUqXWi4w==\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G\n"
|
||||
"A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp\n"
|
||||
"Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4\n"
|
||||
"MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG\n"
|
||||
"A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI\n"
|
||||
"hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8\n"
|
||||
"RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT\n"
|
||||
"gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm\n"
|
||||
"KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd\n"
|
||||
"QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ\n"
|
||||
"XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw\n"
|
||||
"DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o\n"
|
||||
"LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU\n"
|
||||
"RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp\n"
|
||||
"jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK\n"
|
||||
"6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX\n"
|
||||
"mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs\n"
|
||||
"Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH\n"
|
||||
"WD9f\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg\n"
|
||||
"MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh\n"
|
||||
"bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx\n"
|
||||
"MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET\n"
|
||||
"MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ\n"
|
||||
"KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI\n"
|
||||
"xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k\n"
|
||||
"ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD\n"
|
||||
"aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw\n"
|
||||
"LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw\n"
|
||||
"1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX\n"
|
||||
"k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2\n"
|
||||
"SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h\n"
|
||||
"bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n\n"
|
||||
"WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY\n"
|
||||
"rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce\n"
|
||||
"MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD\n"
|
||||
"AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu\n"
|
||||
"bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN\n"
|
||||
"nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt\n"
|
||||
"Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61\n"
|
||||
"55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj\n"
|
||||
"vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf\n"
|
||||
"cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz\n"
|
||||
"oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp\n"
|
||||
"nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs\n"
|
||||
"pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v\n"
|
||||
"JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R\n"
|
||||
"8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4\n"
|
||||
"5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA=\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIICDDCCAZGgAwIBAgIQbkepx2ypcyRAiQ8DVd2NHTAKBggqhkjOPQQDAzBHMQsw\n"
|
||||
"CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU\n"
|
||||
"MBIGA1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw\n"
|
||||
"MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp\n"
|
||||
"Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQA\n"
|
||||
"IgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout\n"
|
||||
"736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2A\n"
|
||||
"DDL24CejQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud\n"
|
||||
"DgQWBBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEAgFuk\n"
|
||||
"fCPAlaUs3L6JbyO5o91lAFJekazInXJ0glMLfalAvWhgxeG4VDvBNhcl2MG9AjEA\n"
|
||||
"njWSdIUlUfUk7GRSJFClH9voy8l27OyCbvWFGFPouOOaKaqW04MjyaR7YbPMAuhd\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL\n"
|
||||
"MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl\n"
|
||||
"eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT\n"
|
||||
"JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx\n"
|
||||
"MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT\n"
|
||||
"Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg\n"
|
||||
"VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm\n"
|
||||
"aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo\n"
|
||||
"I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng\n"
|
||||
"o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G\n"
|
||||
"A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD\n"
|
||||
"VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB\n"
|
||||
"zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW\n"
|
||||
"RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg=\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIFWjCCA0KgAwIBAgIQbkepxlqz5yDFMJo/aFLybzANBgkqhkiG9w0BAQwFADBH\n"
|
||||
"MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM\n"
|
||||
"QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy\n"
|
||||
"MDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl\n"
|
||||
"cnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEB\n"
|
||||
"AQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3Lv\n"
|
||||
"CvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3Kg\n"
|
||||
"GjSY6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9Bu\n"
|
||||
"XvAuMC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOd\n"
|
||||
"re7kRXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXu\n"
|
||||
"PuWgf9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1\n"
|
||||
"mKPV+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K\n"
|
||||
"8YzodDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqj\n"
|
||||
"x5RWIr9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsR\n"
|
||||
"nTKaG73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0\n"
|
||||
"kzCqgc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9Ok\n"
|
||||
"twIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\n"
|
||||
"HQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBALZp\n"
|
||||
"8KZ3/p7uC4Gt4cCpx/k1HUCCq+YEtN/L9x0Pg/B+E02NjO7jMyLDOfxA325BS0JT\n"
|
||||
"vhaI8dI4XsRomRyYUpOM52jtG2pzegVATX9lO9ZY8c6DR2Dj/5epnGB3GFW1fgiT\n"
|
||||
"z9D2PGcDFWEJ+YF59exTpJ/JjwGLc8R3dtyDovUMSRqodt6Sm2T4syzFJ9MHwAiA\n"
|
||||
"pJiS4wGWAqoC7o87xdFtCjMwc3i5T1QWvwsHoaRc5svJXISPD+AVdyx+Jn7axEvb\n"
|
||||
"pxZ3B7DNdehyQtaVhJ2Gg/LkkM0JR9SLA3DaWsYDQvTtN6LwG1BUSw7YhN4ZKJmB\n"
|
||||
"R64JGz9I0cNv4rBgF/XuIwKl2gBbbZCr7qLpGzvpx0QnRY5rn/WkhLx3+WuXrD5R\n"
|
||||
"RaIRpsyF7gpo8j5QOHokYh4XIDdtak23CZvJ/KRY9bb7nE4Yu5UC56GtmwfuNmsk\n"
|
||||
"0jmGwZODUNKBRqhfYlcsu2xkiAhu7xNUX90txGdj08+JN7+dIPT7eoOboB6BAFDC\n"
|
||||
"5AwiWVIQ7UNWhwD4FFKnHYuTjKJNRn8nxnGbJN7k2oaLDX5rIMHAnuFl2GqjpuiF\n"
|
||||
"izoHCBy69Y9Vmhh1fuXsgWbRIXOhNUQLgD1bnF5vKheW0YMjiGZt5obicDIvUiLn\n"
|
||||
"yOd/xCxgXS/Dr55FBcOEArf9LAhST4Ldo/DUhgkC\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh\n"
|
||||
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"
|
||||
"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\n"
|
||||
"MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT\n"
|
||||
"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n"
|
||||
"b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\n"
|
||||
"9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\n"
|
||||
"2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\n"
|
||||
"1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\n"
|
||||
"q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\n"
|
||||
"tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\n"
|
||||
"vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP\n"
|
||||
"BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV\n"
|
||||
"5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY\n"
|
||||
"1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4\n"
|
||||
"NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG\n"
|
||||
"Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91\n"
|
||||
"8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe\n"
|
||||
"pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl\n"
|
||||
"MrY=\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh\n"
|
||||
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"
|
||||
"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\n"
|
||||
"QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT\n"
|
||||
"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n"
|
||||
"b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG\n"
|
||||
"9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB\n"
|
||||
"CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97\n"
|
||||
"nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt\n"
|
||||
"43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P\n"
|
||||
"T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4\n"
|
||||
"gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO\n"
|
||||
"BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR\n"
|
||||
"TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw\n"
|
||||
"DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr\n"
|
||||
"hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg\n"
|
||||
"06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF\n"
|
||||
"PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls\n"
|
||||
"YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk\n"
|
||||
"CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw\n"
|
||||
"CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu\n"
|
||||
"ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe\n"
|
||||
"Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw\n"
|
||||
"EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x\n"
|
||||
"IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF\n"
|
||||
"K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG\n"
|
||||
"fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO\n"
|
||||
"Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd\n"
|
||||
"BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx\n"
|
||||
"AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/\n"
|
||||
"oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8\n"
|
||||
"sycX\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIFWjCCA0KgAwIBAgIQbkepxUtHDA3sM9CJuRz04TANBgkqhkiG9w0BAQwFADBH\n"
|
||||
"MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM\n"
|
||||
"QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy\n"
|
||||
"MDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl\n"
|
||||
"cnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEB\n"
|
||||
"AQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaM\n"
|
||||
"f/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vX\n"
|
||||
"mX7wCl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7\n"
|
||||
"zUjwTcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0P\n"
|
||||
"fyblqAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtc\n"
|
||||
"vfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4\n"
|
||||
"Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUsp\n"
|
||||
"zBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOO\n"
|
||||
"Rc92wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYW\n"
|
||||
"k70paDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+\n"
|
||||
"DVrNVjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgF\n"
|
||||
"lQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\n"
|
||||
"HQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBADiW\n"
|
||||
"Cu49tJYeX++dnAsznyvgyv3SjgofQXSlfKqE1OXyHuY3UjKcC9FhHb8owbZEKTV1\n"
|
||||
"d5iyfNm9dKyKaOOpMQkpAWBz40d8U6iQSifvS9efk+eCNs6aaAyC58/UEBZvXw6Z\n"
|
||||
"XPYfcX3v73svfuo21pdwCxXu11xWajOl40k4DLh9+42FpLFZXvRq4d2h9mREruZR\n"
|
||||
"gyFmxhE+885H7pwoHyXa/6xmld01D1zvICxi/ZG6qcz8WpyTgYMpl0p8WnK0OdC3\n"
|
||||
"d8t5/Wk6kjftbjhlRn7pYL15iJdfOBL07q9bgsiG1eGZbYwE8na6SfZu6W0eX6Dv\n"
|
||||
"J4J2QPim01hcDyxC2kLGe4g0x8HYRZvBPsVhHdljUEn2NIVq4BjFbkerQUIpm/Zg\n"
|
||||
"DdIx02OYI5NaAIFItO/Nis3Jz5nu2Z6qNuFoS3FJFDYoOj0dzpqPJeaAcWErtXvM\n"
|
||||
"+SUWgeExX6GjfhaknBZqlxi9dnKlC54dNuYvoS++cJEPqOba+MSSQGwlfnuzCdyy\n"
|
||||
"F62ARPBopY+Udf90WuioAnwMCeKpSwughQtiue+hMZL77/ZRBIls6Kl0obsXs7X9\n"
|
||||
"SQ98POyDGCBDTtWTurQ0sR8WNh8M5mQ5Fkzc4P4dyKliPUDqysU0ArSuiYgzNdws\n"
|
||||
"E3PYJ/HQcu51OyLemGhmW/HGY0dVHLqlCFF1pkgl\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB\n"
|
||||
"iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl\n"
|
||||
"cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV\n"
|
||||
"BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw\n"
|
||||
"MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV\n"
|
||||
"BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU\n"
|
||||
"aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy\n"
|
||||
"dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\n"
|
||||
"AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B\n"
|
||||
"3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY\n"
|
||||
"tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/\n"
|
||||
"Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2\n"
|
||||
"VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT\n"
|
||||
"79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6\n"
|
||||
"c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT\n"
|
||||
"Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l\n"
|
||||
"c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee\n"
|
||||
"UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE\n"
|
||||
"Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd\n"
|
||||
"BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G\n"
|
||||
"A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF\n"
|
||||
"Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO\n"
|
||||
"VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3\n"
|
||||
"ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs\n"
|
||||
"8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR\n"
|
||||
"iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze\n"
|
||||
"Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ\n"
|
||||
"XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/\n"
|
||||
"qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB\n"
|
||||
"VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB\n"
|
||||
"L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG\n"
|
||||
"jjxDah2nGN59PRbxYvnKkKj9\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx\n"
|
||||
"EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT\n"
|
||||
"HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs\n"
|
||||
"ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5\n"
|
||||
"MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD\n"
|
||||
"VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy\n"
|
||||
"ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy\n"
|
||||
"dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI\n"
|
||||
"hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p\n"
|
||||
"OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2\n"
|
||||
"8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K\n"
|
||||
"Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe\n"
|
||||
"hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk\n"
|
||||
"6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw\n"
|
||||
"DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q\n"
|
||||
"AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI\n"
|
||||
"bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB\n"
|
||||
"ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z\n"
|
||||
"qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd\n"
|
||||
"iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn\n"
|
||||
"0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN\n"
|
||||
"sSi6\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n"
|
||||
"TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n"
|
||||
"cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\n"
|
||||
"WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\n"
|
||||
"ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\n"
|
||||
"MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\n"
|
||||
"h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\n"
|
||||
"0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\n"
|
||||
"A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\n"
|
||||
"T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\n"
|
||||
"B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\n"
|
||||
"B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\n"
|
||||
"KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\n"
|
||||
"OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\n"
|
||||
"jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\n"
|
||||
"qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\n"
|
||||
"rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\n"
|
||||
"HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\n"
|
||||
"hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\n"
|
||||
"ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\n"
|
||||
"3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\n"
|
||||
"NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\n"
|
||||
"ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\n"
|
||||
"TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\n"
|
||||
"jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\n"
|
||||
"oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\n"
|
||||
"4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\n"
|
||||
"mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\n"
|
||||
"emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi\n"
|
||||
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"
|
||||
"d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg\n"
|
||||
"RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV\n"
|
||||
"UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu\n"
|
||||
"Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG\n"
|
||||
"SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y\n"
|
||||
"ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If\n"
|
||||
"xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV\n"
|
||||
"ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO\n"
|
||||
"DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ\n"
|
||||
"jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/\n"
|
||||
"CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi\n"
|
||||
"EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM\n"
|
||||
"fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY\n"
|
||||
"uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK\n"
|
||||
"chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t\n"
|
||||
"9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\n"
|
||||
"hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD\n"
|
||||
"ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2\n"
|
||||
"SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd\n"
|
||||
"+SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc\n"
|
||||
"fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa\n"
|
||||
"sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N\n"
|
||||
"cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N\n"
|
||||
"0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie\n"
|
||||
"4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI\n"
|
||||
"r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1\n"
|
||||
"/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm\n"
|
||||
"gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG\n"
|
||||
"A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv\n"
|
||||
"b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw\n"
|
||||
"MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i\n"
|
||||
"YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT\n"
|
||||
"aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ\n"
|
||||
"jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp\n"
|
||||
"xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp\n"
|
||||
"1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG\n"
|
||||
"snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ\n"
|
||||
"U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8\n"
|
||||
"9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E\n"
|
||||
"BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B\n"
|
||||
"AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz\n"
|
||||
"yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE\n"
|
||||
"38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP\n"
|
||||
"AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad\n"
|
||||
"DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME\n"
|
||||
"HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\n"
|
||||
"ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\n"
|
||||
"b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\n"
|
||||
"MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\n"
|
||||
"b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\n"
|
||||
"ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n"
|
||||
"9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\n"
|
||||
"IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\n"
|
||||
"VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n"
|
||||
"93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\n"
|
||||
"jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\n"
|
||||
"AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\n"
|
||||
"A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\n"
|
||||
"U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\n"
|
||||
"N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\n"
|
||||
"o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\n"
|
||||
"5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\n"
|
||||
"rqXRfboQnoZsG4q5WTP468SQvvG5\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQGEwJV\n"
|
||||
"UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg\n"
|
||||
"UjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE\n"
|
||||
"ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0G\n"
|
||||
"CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaM\n"
|
||||
"f/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7wCl7raKb0\n"
|
||||
"xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjwTcLCeoiKu7rPWRnWr4+w\n"
|
||||
"B7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0PfyblqAj+lug8aJRT7oM6iCsVlgmy4HqMLnXW\n"
|
||||
"nOunVmSPlk9orj2XwoSPwLxAwAtcvfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk\n"
|
||||
"9+aCEI3oncKKiPo4Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zq\n"
|
||||
"kUspzBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92wO1A\n"
|
||||
"K/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70paDPvOmbsB4om3xPX\n"
|
||||
"V2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrNVjzRlwW5y0vtOUucxD/SVRNuJLDW\n"
|
||||
"cfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0T\n"
|
||||
"AQH/BAUwAwEB/zAdBgNVHQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQAD\n"
|
||||
"ggIBAJ+qQibbC5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe\n"
|
||||
"QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuyh6f88/qBVRRi\n"
|
||||
"ClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM47HLwEXWdyzRSjeZ2axfG34ar\n"
|
||||
"J45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8JZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYci\n"
|
||||
"NuaCp+0KueIHoI17eko8cdLiA6EfMgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5me\n"
|
||||
"LMFrUKTX5hgUvYU/Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJF\n"
|
||||
"fbdT6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ0E6yove+\n"
|
||||
"7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm2tIMPNuzjsmhDYAPexZ3\n"
|
||||
"FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bbbP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3\n"
|
||||
"gm3c\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJVUzEi\n"
|
||||
"MCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMw\n"
|
||||
"HhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZ\n"
|
||||
"R29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjO\n"
|
||||
"PQIBBgUrgQQAIgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout\n"
|
||||
"736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL24CejQjBA\n"
|
||||
"MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTB8Sa6oC2uhYHP0/Eq\n"
|
||||
"Er24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azT\n"
|
||||
"L818+FsuVbu/3ZL3pAzcMeGiAjEA/JdmZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV\n"
|
||||
"11RZt+cRLInUue4X\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJVUzEi\n"
|
||||
"MCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQw\n"
|
||||
"HhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZ\n"
|
||||
"R29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjO\n"
|
||||
"PQIBBgUrgQQAIgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzu\n"
|
||||
"hXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvRHYqjQjBA\n"
|
||||
"MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSATNbrdP9JNqPV2Py1\n"
|
||||
"PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/C\n"
|
||||
"r8deVl5c1RxYIigL9zC2L7F8AjEA8GE8p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh\n"
|
||||
"4rsUecrNIdSUtUlD\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQswCQYDVQQGEwJH\n"
|
||||
"QjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBTZXJ2\n"
|
||||
"ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5\n"
|
||||
"WjBfMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0\n"
|
||||
"aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUr\n"
|
||||
"gQQAIgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccCWvkEN/U0\n"
|
||||
"NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+6xnOQ6OjQjBAMB0GA1Ud\n"
|
||||
"DgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB\n"
|
||||
"/zAKBggqhkjOPQQDAwNnADBkAjAn7qRaqCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RH\n"
|
||||
"lAFWovgzJQxC36oCMB3q4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21U\n"
|
||||
"SAGKcw==\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBfMQswCQYDVQQG\n"
|
||||
"EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT\n"
|
||||
"ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwHhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1\n"
|
||||
"OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T\n"
|
||||
"ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3\n"
|
||||
"DQEBAQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDaef0rty2k\n"
|
||||
"1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnzSDBh+oF8HqcIStw+Kxwf\n"
|
||||
"GExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xfiOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMP\n"
|
||||
"FF1bFOdLvt30yNoDN9HWOaEhUTCDsG3XME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vu\n"
|
||||
"ZDCQOc2TZYEhMbUjUDM3IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5Qaz\n"
|
||||
"Yw6A3OASVYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgESJ/A\n"
|
||||
"wSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu+Zd4KKTIRJLpfSYF\n"
|
||||
"plhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt8uaZFURww3y8nDnAtOFr94MlI1fZ\n"
|
||||
"EoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+LHaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW\n"
|
||||
"6aWWrL3DkJiy4Pmi1KZHQ3xtzwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWI\n"
|
||||
"IUkwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c\n"
|
||||
"mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQYKlJfp/imTYp\n"
|
||||
"E0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52gDY9hAaLMyZlbcp+nv4fjFg4\n"
|
||||
"exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZAFv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M\n"
|
||||
"0ejf5lG5Nkc/kLnHvALcWxxPDkjBJYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI\n"
|
||||
"84HxZmduTILA7rpXDhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9m\n"
|
||||
"pFuiTdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5dHn5Hrwd\n"
|
||||
"Vw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65LvKRRFHQV80MNNVIIb/b\n"
|
||||
"E/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmm\n"
|
||||
"J1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAYQqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL\n"
|
||||
"-----END CERTIFICATE-----\n"
|
||||
;
|
||||
#endif
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
/* cfg.c - tiny JSON config via jsonlite */
|
||||
#include "cfg.h"
|
||||
#include "jsonlite.h"
|
||||
#include "log.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
cfg_t g_cfg;
|
||||
|
||||
void cfg_defaults(cfg_t *c) {
|
||||
if(!c) return;
|
||||
memset(c, 0, sizeof(*c));
|
||||
c->schema_version = CFG_SCHEMA_VERSION;
|
||||
c->enabled = 1;
|
||||
c->auto_update = 1;
|
||||
c->poll_interval_s = 12;
|
||||
strncpy(c->token, "SET_ME", sizeof(c->token)-1);
|
||||
strncpy(c->presence_state, "On PS4", sizeof(c->presence_state)-1);
|
||||
/* Idle tile ships working: project-hosted orbisRPC logo, resolved
|
||||
* through the mp: proxy like every other art URL. Override with any
|
||||
* http(s) URL or uploaded asset key. */
|
||||
strncpy(c->home_art, "https://raw.githubusercontent.com/SirHumza/orbisRPC/main/config/icons/logo.png",
|
||||
sizeof(c->home_art)-1);
|
||||
c->n_titles = 0;
|
||||
/* Default art backend: our own Sony-CDN icon pack, resolved through
|
||||
* Discord's external-assets proxy (mp:) at post time. Works from the
|
||||
* start with zero setup; missing titles degrade to no art. */
|
||||
strncpy(c->art_base_url, "https://raw.githubusercontent.com/SirHumza/orbisrpc-host/main/icons/",
|
||||
sizeof(c->art_base_url)-1);
|
||||
strncpy(c->application_id, "1536977374795538532", sizeof(c->application_id)-1);
|
||||
}
|
||||
|
||||
/* keep the daemon sane if the user puts junk in config */
|
||||
static void clamp_cfg(cfg_t *c){
|
||||
if(c->poll_interval_s < 5){
|
||||
log_msg("config: poll_interval_s %d too small; using 5", c->poll_interval_s);
|
||||
c->poll_interval_s = 5;
|
||||
}
|
||||
if(c->poll_interval_s > 300){
|
||||
log_msg("config: poll_interval_s %d too large; using 300", c->poll_interval_s);
|
||||
c->poll_interval_s = 300;
|
||||
}
|
||||
if(c->token[0] && !strcmp(c->token, "SET_ME"))
|
||||
log_msg("config: token is still the SET_ME placeholder");
|
||||
}
|
||||
|
||||
int cfg_load(const char *path, cfg_t *c) {
|
||||
if(!path || !c) return -1;
|
||||
cfg_defaults(c);
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) { log_msg("config not found at %s; defaults applied", path); return -1; }
|
||||
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return -1; }
|
||||
long sz = ftell(f);
|
||||
if (sz < 0 || fseek(f, 0, SEEK_SET) != 0) { fclose(f); return -1; }
|
||||
if (sz <= 0 || sz > 1<<20) { fclose(f); return -1; }
|
||||
char *buf = (char *)malloc((size_t)sz + 1);
|
||||
if (!buf) { fclose(f); return -1; }
|
||||
size_t n = fread(buf, 1, (size_t)sz, f); fclose(f);
|
||||
if(n != (size_t)sz){ free(buf); log_msg("config read failed at %s", path); return -1; }
|
||||
buf[n] = 0;
|
||||
jl_val_t *root = jl_parse(buf, n);
|
||||
free(buf);
|
||||
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); \
|
||||
c->f[sizeof(c->f)-1]=0; \
|
||||
} \
|
||||
} while(0)
|
||||
STR("token", token);
|
||||
STR("application_id", application_id);
|
||||
STR("art_base_url", art_base_url);
|
||||
STR("home_art", home_art);
|
||||
STR("presence_state", presence_state);
|
||||
#undef STR
|
||||
/* User-local title overrides: {"CUSA11995": "Marvel's Spider-Man"}.
|
||||
* Defensive: wrong types, overlong keys/names, and overflow past
|
||||
* CFG_MAX_TITLES are ignored, never fatal. */
|
||||
c->n_titles = 0;
|
||||
o = jl_obj_get(root, "titles");
|
||||
if(o && o->type == JL_OBJECT){
|
||||
for(jl_val_t *p = o->child; p && c->n_titles < CFG_MAX_TITLES; p = p->next){
|
||||
const jl_val_t *v = p->child ? p->child : p;
|
||||
if(!p->str || !v || v->type != JL_STRING || !v->str) continue;
|
||||
size_t kl = strlen(p->str), vl = strlen(v->str);
|
||||
if(kl == 0 || kl >= (size_t)CFG_TITLEID_LEN) continue;
|
||||
if(vl < 2 || vl >= (size_t)CFG_TITLENAME_LEN) continue;
|
||||
int ok = 1;
|
||||
for(size_t i = 0; i < kl; i++){
|
||||
char ch = p->str[i];
|
||||
if(!((ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_')){ ok = 0; break; }
|
||||
}
|
||||
if(!ok) continue;
|
||||
strncpy(c->title_ids[c->n_titles], p->str, CFG_TITLEID_LEN - 1);
|
||||
c->title_ids[c->n_titles][CFG_TITLEID_LEN - 1] = 0;
|
||||
strncpy(c->title_names[c->n_titles], v->str, CFG_TITLENAME_LEN - 1);
|
||||
c->title_names[c->n_titles][CFG_TITLENAME_LEN - 1] = 0;
|
||||
c->n_titles++;
|
||||
}
|
||||
}
|
||||
o = jl_obj_get(root, "enabled"); if (o && o->type == JL_BOOL) c->enabled = (int)o->num;
|
||||
o = jl_obj_get(root, "auto_update"); if (o && o->type == JL_BOOL) c->auto_update = (int)o->num;
|
||||
o = jl_obj_get(root, "debug"); if (o && o->type == JL_BOOL) c->debug = (int)o->num;
|
||||
o = jl_obj_get(root, "poll_interval_s"); if (o && o->type == JL_NUMBER) c->poll_interval_s = (int)o->num;
|
||||
o = jl_obj_get(root, "schema_version"); if (o && o->type == JL_NUMBER) c->schema_version = (int)o->num;
|
||||
jl_free(root);
|
||||
clamp_cfg(c);
|
||||
/* Migration: stamp current schema so re-saves converge.
|
||||
* v0 (no key): identical layout, adopt as-is. */
|
||||
c->schema_version = CFG_SCHEMA_VERSION;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cfg_title(const cfg_t *c, const char *titleId, char *out, size_t cap){
|
||||
if(!c || !titleId || !titleId[0] || !out || cap == 0) return -1;
|
||||
for(int i = 0; i < c->n_titles; i++){
|
||||
if(!strcmp(c->title_ids[i], titleId)){
|
||||
strncpy(out, c->title_names[i], cap - 1);
|
||||
out[cap - 1] = 0;
|
||||
return out[0] ? 0 : -1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Self-learning map (PC-tool "mapped" pattern): remember an authoritatively
|
||||
* resolved name so later boots resolve instantly even when every live
|
||||
* source is unreachable. Never learns raw-ID echoes or junk.
|
||||
* Returns 1 when the map changed (caller should cfg_save). */
|
||||
int cfg_learn(cfg_t *c, const char *titleId, const char *name){
|
||||
if(!c || !titleId || !name) return 0;
|
||||
size_t kl = strlen(titleId), vl = strlen(name);
|
||||
if(kl == 0 || kl >= (size_t)CFG_TITLEID_LEN) return 0;
|
||||
if(vl < 2 || vl >= (size_t)CFG_TITLENAME_LEN) return 0;
|
||||
if(!strcmp(titleId, name)) return 0; /* ID echo, not a name */
|
||||
for(size_t i = 0; i < kl; i++){
|
||||
char ch = titleId[i];
|
||||
if(!((ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_')) return 0;
|
||||
}
|
||||
for(int i = 0; i < c->n_titles; i++){
|
||||
if(!strcmp(c->title_ids[i], titleId)){
|
||||
if(!strcmp(c->title_names[i], name)) return 0;
|
||||
strncpy(c->title_names[i], name, CFG_TITLENAME_LEN - 1);
|
||||
c->title_names[i][CFG_TITLENAME_LEN - 1] = 0;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if(c->n_titles >= CFG_MAX_TITLES){
|
||||
log_msg("config: titles map full; not learning %s", titleId);
|
||||
return 0;
|
||||
}
|
||||
strncpy(c->title_ids[c->n_titles], titleId, CFG_TITLEID_LEN - 1);
|
||||
c->title_ids[c->n_titles][CFG_TITLEID_LEN - 1] = 0;
|
||||
strncpy(c->title_names[c->n_titles], name, CFG_TITLENAME_LEN - 1);
|
||||
c->title_names[c->n_titles][CFG_TITLENAME_LEN - 1] = 0;
|
||||
c->n_titles++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int cfg_save(const char *path, const cfg_t *c) {
|
||||
if(!path || !c) return -1;
|
||||
jl_val_t *r = jl_new_object();
|
||||
if(!r) { log_msg("cfg_save: allocation failed"); return -1; }
|
||||
jl_obj_set(r, "schema_version", jl_new_number((double)CFG_SCHEMA_VERSION));
|
||||
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, "art_base_url", jl_new_string(c->art_base_url));
|
||||
jl_obj_set(r, "enabled", jl_new_bool(c->enabled));
|
||||
jl_obj_set(r, "auto_update", jl_new_bool(c->auto_update));
|
||||
jl_obj_set(r, "debug", jl_new_bool(c->debug));
|
||||
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, "home_art", jl_new_string(c->home_art));
|
||||
if(c->n_titles > 0){
|
||||
jl_val_t *t = jl_new_object();
|
||||
if(t){
|
||||
for(int i = 0; i < c->n_titles; i++)
|
||||
jl_obj_set(t, c->title_ids[i], jl_new_string(c->title_names[i]));
|
||||
jl_obj_set(r, "titles", t);
|
||||
}
|
||||
}
|
||||
char *s = jl_stringify(r);
|
||||
if(!s){ log_msg("cfg_save: serialization failed"); jl_free(r); return -1; }
|
||||
/* write tmp + fsync + rename so a power loss can't corrupt the config */
|
||||
char tmp[160];
|
||||
snprintf(tmp, sizeof tmp, "%s.tmp", path);
|
||||
FILE *f = fopen(tmp, "wb");
|
||||
int ok = 0;
|
||||
if (f) {
|
||||
ok = (fputs(s, f) >= 0);
|
||||
if(fflush(f) != 0) ok = 0;
|
||||
/* force bytes to disk before rename */
|
||||
if(ok) { int fd = fileno(f); if(fd >= 0 && fsync(fd) != 0) ok = 0; }
|
||||
if(fclose(f) != 0) ok = 0;
|
||||
if(ok && rename(tmp, path) != 0) ok = 0;
|
||||
if(!ok){
|
||||
remove(tmp);
|
||||
log_msg("cfg_save: write failed for %s", path);
|
||||
}
|
||||
} else { log_msg("cfg_save: cannot write %s", tmp); }
|
||||
free(s); jl_free(r);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/* cfg.h - tiny JSON config via jsonlite */
|
||||
#ifndef CFG_H
|
||||
#define CFG_H
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#define CFG_PATH "/data/orbisRPC/config.json"
|
||||
#define LOG_PATH "/data/orbisRPC/log.txt"
|
||||
#define DATA_DIR "/data/orbisRPC"
|
||||
/* Config schema version. Stamp on load so re-saves converge; unknown
|
||||
* future versions load defensively (known fields only). */
|
||||
#define CFG_SCHEMA_VERSION 1
|
||||
/* User-local title overrides: {"CUSA11995": "Marvel's Spider-Man"}.
|
||||
* Lives in the user's own config.json (never shipped) for games no
|
||||
* on-box source can name (no pronunciation.xml, TMDB blocked, ...).
|
||||
* Fixed-size tables: no malloc, PS4-safe. */
|
||||
#define CFG_MAX_TITLES 64
|
||||
#define CFG_TITLEID_LEN 16
|
||||
#define CFG_TITLENAME_LEN 128
|
||||
typedef struct {
|
||||
int schema_version;
|
||||
char token[512]; /* Discord user session token */
|
||||
char application_id[64]; /* optional: app id for uploaded asset images */
|
||||
char art_base_url[256]; /* optional: icon pack base URL, e.g.
|
||||
* https://raw.githubusercontent.com/.../icons/
|
||||
* large_image becomes <base><lower titleId>.png */
|
||||
char home_art[256]; /* optional: idle/home tile art. http(s) URL
|
||||
* (mp:-proxied) or uploaded Discord asset key.
|
||||
* Default: project-hosted PlayStation logo.
|
||||
* Empty falls back to <art_base_url>home.png. */
|
||||
int n_titles;
|
||||
char title_ids[CFG_MAX_TITLES][CFG_TITLEID_LEN];
|
||||
char title_names[CFG_MAX_TITLES][CFG_TITLENAME_LEN];
|
||||
int enabled;
|
||||
int auto_update; /* check GitHub releases once per boot, stage newer */
|
||||
int debug; /* verbose debug logging (per-poll detail, no secrets) */
|
||||
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);
|
||||
/* Atomic save (tmp + fsync + rename). 0 saved, -1 failed (caller must
|
||||
* surface, not assume). */
|
||||
int cfg_save(const char *path, const cfg_t *c);
|
||||
void cfg_defaults(cfg_t *c);
|
||||
/* User override lookup: 0 + name copied when titleId has an entry. */
|
||||
int cfg_title(const cfg_t *c, const char *titleId, char *out, size_t cap);
|
||||
/* Learn an authoritatively resolved name (1 = changed, save it). */
|
||||
int cfg_learn(cfg_t *c, const char *titleId, const char *name);
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
/* clock.h - monotonic time for measuring durations.
|
||||
*
|
||||
* Rule: wall clock (time(NULL)) is ONLY for values that must be wall
|
||||
* clock (Discord epoch timestamps, log stamps). Every duration/deadline
|
||||
* uses orbis_mono_s() so a clock jump can never break local timing.
|
||||
*
|
||||
* PS4: sceKernelGetProcessTime (microseconds, monotonic).
|
||||
* Host: clock_gettime(CLOCK_MONOTONIC). */
|
||||
#ifndef ORBISRPC_CLOCK_H
|
||||
#define ORBISRPC_CLOCK_H
|
||||
#include <stdint.h>
|
||||
/* Monotonic seconds (arbitrary epoch, never goes backward). */
|
||||
int64_t orbis_mono_s(void);
|
||||
#endif
|
||||
@@ -0,0 +1,125 @@
|
||||
/* compat.c - tiny libc shims for the PS4 link environment.
|
||||
* SceLibcInternal lacks getentropy/__errno_location/gmtime_r, and
|
||||
* mbedTLS wants a platform entropy poll. Compiled into payload + plugin. */
|
||||
#include <stddef.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
#include <stdint.h>
|
||||
#include "clock.h"
|
||||
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
/* Payload-SDK builds: the SDK libc already provides errno, gmtime_r,
|
||||
* clock_gettime and friends. Only the entropy poll + monotonic clock
|
||||
* live here, written against plain POSIX. */
|
||||
int mbedtls_platform_entropy_poll(void *data, unsigned char *out,
|
||||
size_t len, size_t *olen){
|
||||
(void)data;
|
||||
if(!out || len == 0) return -1;
|
||||
int fd = open("/dev/urandom", O_RDONLY);
|
||||
if(fd < 0) return -1;
|
||||
size_t got = 0;
|
||||
while(got < len){
|
||||
long r = read(fd, (char *)out + got, len - got);
|
||||
if(r <= 0){ close(fd); return -1; }
|
||||
got += (size_t)r;
|
||||
}
|
||||
close(fd);
|
||||
*olen = len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int64_t orbis_mono_s(void){
|
||||
struct timespec ts;
|
||||
if(clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return (int64_t)time(NULL);
|
||||
return (int64_t)ts.tv_sec;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
int *__errno_location(void) __attribute__((weak));
|
||||
int *__errno_location(void){
|
||||
static int errno_slot;
|
||||
return &errno_slot;
|
||||
}
|
||||
|
||||
__attribute__((weak)) int getentropy(void *buf, size_t n){
|
||||
int fd = open("/dev/urandom", O_RDONLY);
|
||||
if(fd < 0) return -1;
|
||||
size_t got = 0;
|
||||
while(got < n){
|
||||
long r = read(fd, (char*)buf + got, n - got);
|
||||
if(r < 0 && errno == EINTR) continue; /* signal delivery, not failure */
|
||||
if(r <= 0){ close(fd); return -1; }
|
||||
got += (size_t)r;
|
||||
}
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
__attribute__((weak)) struct tm *gmtime_r(const time_t *t, struct tm *r){
|
||||
/* Thread-safe: pure arithmetic, no static buffer (gmtime() is shared). */
|
||||
if(!t || !r) return NULL;
|
||||
long long secs = (long long)*t;
|
||||
long long days = secs / 86400LL;
|
||||
long long rem = secs % 86400LL;
|
||||
if(rem < 0){ rem += 86400LL; days--; }
|
||||
r->tm_hour = (int)(rem / 3600); rem %= 3600;
|
||||
r->tm_min = (int)(rem / 60);
|
||||
r->tm_sec = (int)(rem % 60);
|
||||
/* civil_from_days (Howard Hinnant) */
|
||||
long long z = days + 719468LL;
|
||||
long long era = (z >= 0 ? z : z - 146096LL) / 146097LL;
|
||||
unsigned doe = (unsigned)(z - era * 146097LL);
|
||||
unsigned yoe = (doe - doe/1460U + doe/36524U - doe/146096U) / 365U;
|
||||
long long y = (long long)yoe + era * 400LL;
|
||||
unsigned doy = doe - (365U*yoe + yoe/4U - yoe/100U);
|
||||
unsigned mp = (5U*doy + 2U)/153U;
|
||||
unsigned d = doy - (153U*mp+2U)/5U + 1U;
|
||||
unsigned m = mp + (mp < 10U ? 3U : (unsigned)-9);
|
||||
y += (m <= 2);
|
||||
r->tm_mday = (int)d;
|
||||
r->tm_mon = (int)(m - 1);
|
||||
r->tm_year = (int)(y - 1900LL);
|
||||
/* weekday: 1970-01-01 was Thursday */
|
||||
r->tm_wday = (int)((days % 7 + 7 + 4) % 7);
|
||||
/* yearday */
|
||||
{
|
||||
int leap = ((y%4==0 && y%100!=0) || y%400==0);
|
||||
static const int cum[12] = {0,31,59,90,120,151,181,212,243,273,304,334};
|
||||
r->tm_yday = cum[r->tm_mon] + (int)d - 1 + (leap && r->tm_mon > 1 ? 1 : 0);
|
||||
}
|
||||
r->tm_isdst = 0;
|
||||
return r;
|
||||
}
|
||||
|
||||
/* mbedTLS platform entropy: /dev/urandom, the only strong source here. */
|
||||
int mbedtls_platform_entropy_poll(void *data, unsigned char *out,
|
||||
size_t len, size_t *olen){
|
||||
(void)data;
|
||||
if(!out || len == 0) return -1;
|
||||
if(getentropy(out, len) != 0) return -1;
|
||||
*olen = len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int64_t orbis_mono_s(void){
|
||||
#if defined(__PS4__) || (defined(__FreeBSD__) && !defined(__APPLE__))
|
||||
/* PS4 builds (--target=x86_64-pc-freebsd12-elf, -D__PS4__). Declared
|
||||
* here instead of including orbis headers so host builds stay clean. */
|
||||
extern unsigned long long sceKernelGetProcessTime(void);
|
||||
return (int64_t)(sceKernelGetProcessTime() / 1000000ull);
|
||||
#else
|
||||
struct timespec ts;
|
||||
#if defined(CLOCK_MONOTONIC)
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
#else
|
||||
ts.tv_sec = time(NULL);
|
||||
ts.tv_nsec = 0;
|
||||
#endif
|
||||
return (int64_t)ts.tv_sec;
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif /* ORBISRPC_SDK_PAYLOAD */
|
||||
@@ -0,0 +1,587 @@
|
||||
/* daemon.c - orbisRPC core loop, usable from the ELF payload (main.c) or from a
|
||||
* GoldHEN plugin. When `fixed_game_name` is non-NULL the daemon posts presence
|
||||
* for that game unconditionally (the plugin runs inside the game process and
|
||||
* already knows the title); otherwise it detects the foreground game.
|
||||
*
|
||||
* 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 "clock.h"
|
||||
#include "lock.h"
|
||||
#include "timesync.h"
|
||||
#include "health.h"
|
||||
#include "log.h"
|
||||
#include "ws.h"
|
||||
#include "discord.h"
|
||||
#include "detect.h"
|
||||
#include "updater.h"
|
||||
#include "version.h"
|
||||
#include "jsonlite.h"
|
||||
#include "art.h"
|
||||
#include <sys/stat.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
extern cfg_t g_cfg;
|
||||
|
||||
static volatile sig_atomic_t s_stop = 0;
|
||||
|
||||
/* Tells a running daemon_run() to shut down (called from plugin_unload). */
|
||||
void daemon_request_stop(void){ s_stop = 1; }
|
||||
void daemon_clear_stop(void){ s_stop = 0; }
|
||||
int daemon_stop_requested(void){ return s_stop; }
|
||||
|
||||
/* SIGTERM/SIGINT (payload managers, kill): set the flag only — the loops
|
||||
* unwind through their normal cleanup (presence clear, session save,
|
||||
* socket close) instead of dying mid-write. */
|
||||
static void daemon_on_signal(int sig){
|
||||
(void)sig;
|
||||
s_stop = 1;
|
||||
}
|
||||
|
||||
/* Sleep up to `secs` but wake within a second when the plugin asks us to
|
||||
* stop, so plugin_unload() never hangs on a long backoff. Returns 1 stopped. */
|
||||
static int sleep_stop(int secs){
|
||||
for(int i = 0; i < secs; i++){
|
||||
if(s_stop) return 1;
|
||||
sleep(1);
|
||||
}
|
||||
return s_stop;
|
||||
}
|
||||
|
||||
static int have_token(const cfg_t *c){
|
||||
if(!c || !c->token[0]) return 0;
|
||||
if(strcmp(c->token,"SET_ME")==0) return 0;
|
||||
/* reject whitespace-only / trivially short tokens */
|
||||
size_t n = strlen(c->token);
|
||||
if(n < 16) return 0;
|
||||
for(size_t i = 0; i < n; i++){
|
||||
if(c->token[i]!=' ' && c->token[i]!='\t' && c->token[i]!='\r' && c->token[i]!='\n')
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Persist the live session (atomic tmp+rename). Called on every
|
||||
* transition and on clear so a restart resumes instead of resetting. */
|
||||
static void sess_save(const char *tid, const char *name, int64_t started){ jl_val_t *r = jl_new_object();
|
||||
if(!r) return;
|
||||
jl_obj_set(r, "title_id", jl_new_string(tid ? tid : ""));
|
||||
jl_obj_set(r, "name", jl_new_string(name ? name : ""));
|
||||
jl_obj_set(r, "started", jl_new_number((double)started));
|
||||
jl_obj_set(r, "saved_at", jl_new_number((double)time(NULL)));
|
||||
char *s = jl_stringify(r);
|
||||
jl_free(r);
|
||||
if(!s) return;
|
||||
FILE *f = fopen("/data/orbisRPC/session.json.new", "wb");
|
||||
if(f){
|
||||
int ok = (fputs(s, f) >= 0) && (fflush(f) == 0);
|
||||
if(fclose(f) != 0) ok = 0;
|
||||
if(ok) rename("/data/orbisRPC/session.json.new",
|
||||
"/data/orbisRPC/session.json");
|
||||
else remove("/data/orbisRPC/session.json.new");
|
||||
}
|
||||
free(s);
|
||||
}
|
||||
|
||||
/* Playtime ledger: append-only "<title_id> <name> <seconds>" per finished
|
||||
* session. Totals are computed by readers (app/docs); the daemon only
|
||||
* appends, so a corrupt ledger can never break the runtime. */
|
||||
static void ledger_append(const char *tid, const char *name,
|
||||
int64_t started, int64_t ended){
|
||||
if(!tid || !tid[0] || ended <= started || started <= 0) return;
|
||||
FILE *f = fopen("/data/orbisRPC/playtime.log", "ab");
|
||||
if(!f) return;
|
||||
fprintf(f, "%s %s %lld\n", tid, (name && name[0]) ? name : "?",
|
||||
(long long)(ended - started));
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* Deterministic jitter ±20% without RNG state: hash of mono time and a
|
||||
* counter. Keeps reconnect storms decorrelated across restarts. */
|
||||
static int backoff_jitter(int base, unsigned *ctr){
|
||||
unsigned x = (unsigned)orbis_mono_s() * 2654435761u + (++(*ctr)) * 40503u;
|
||||
x ^= x >> 15; x *= 0x2c1b3c6du; x ^= x >> 12;
|
||||
int pct = 80 + (int)(x % 41); /* 80..120 */
|
||||
return (base * pct) / 100;
|
||||
}
|
||||
|
||||
/* Next reconnect delay: exponential from base, cap 60s for the first 10
|
||||
* consecutive failures, then 10-minute cadence (quiet persistence, never
|
||||
* exit). Returns the delay; bumps *fails. */
|
||||
static int reconnect_delay(int *fails, int base, unsigned *jctr){
|
||||
int n = ++(*fails);
|
||||
int b = base;
|
||||
for(int i = 1; i < n && b < 60; i++) b *= 2;
|
||||
if(b > 60) b = 60;
|
||||
if(n > 10) b = 600;
|
||||
return backoff_jitter(b, jctr);
|
||||
}
|
||||
|
||||
/* Explicit presence state (logged on every transition; no ambiguous
|
||||
* states): NONE -> HOME <-> GAME, with reconnects re-posting. */
|
||||
typedef enum { PS_NONE = 0, PS_HOME, PS_GAME } pres_state_t;
|
||||
static const char *pres_name(pres_state_t s){
|
||||
return s == PS_GAME ? "GAME" : s == PS_HOME ? "HOME" : "NONE";
|
||||
}
|
||||
static void pres_set(pres_state_t *cur, pres_state_t next){
|
||||
if(*cur == next) return;
|
||||
log_msg("STATE: presence %s -> %s", pres_name(*cur), pres_name(next));
|
||||
*cur = next;
|
||||
}
|
||||
/* fixed_game_name != NULL -> post presence for that game only, no detection.
|
||||
* 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;
|
||||
#ifdef SIGTERM
|
||||
signal(SIGTERM, daemon_on_signal);
|
||||
#endif
|
||||
#ifdef SIGINT
|
||||
signal(SIGINT, daemon_on_signal);
|
||||
#endif
|
||||
if(mkdir(DATA_DIR, 0777) != 0){
|
||||
/* EEXIST is fine; anything else means payload can't persist config/log */
|
||||
struct stat st;
|
||||
if(stat(DATA_DIR, &st) != 0){
|
||||
log_init(LOG_PATH);
|
||||
log_msg("FATAL: cannot create %s; check /data writable", DATA_DIR);
|
||||
log_close();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
log_init(LOG_PATH);
|
||||
log_msg("orbisRPC daemon start — build %s %s", __DATE__, __TIME__);
|
||||
/* Single writer: a second launch (or a stale pileup from repeated
|
||||
* injections) stands down instead of fighting over the gateway. */
|
||||
{
|
||||
int lr = lock_acquire();
|
||||
if(lr == 1){ log_msg("another daemon holds the lock; standing down"); log_close(); return 0; }
|
||||
if(lr != 0) log_msg("WARN: lock error; continuing without guard");
|
||||
}
|
||||
/* Crash recovery: unclean-boot marker + consecutive-failure counter.
|
||||
* Only boots that die before going healthy count; normal reboots and
|
||||
* token-less idles never trip safe mode. */
|
||||
int safe_mode = health_boot_note_crash();
|
||||
if(safe_mode)
|
||||
log_msg("WARN: repeated unclean boots; safe mode (updates off)");
|
||||
/* Boot watchdog: a staged update that never proved itself healthy
|
||||
* gets rolled back to .bak before anything runs it. */
|
||||
{
|
||||
static const char *targets[] = {
|
||||
"/data/payloads/orbisrpc.bin",
|
||||
"/data/GoldHEN/plugins/orbisrpc_plugin.prx",
|
||||
};
|
||||
for(unsigned ti = 0; ti < sizeof targets/sizeof targets[0]; ti++){
|
||||
int vr = health_verify_or_rollback(targets[ti]);
|
||||
if(vr == 1)
|
||||
log_msg("WARN: %s rolled back to last-good backup", targets[ti]);
|
||||
else if(vr == -1)
|
||||
log_msg("WARN: %s failed verification (no backup)", targets[ti]);
|
||||
}
|
||||
}
|
||||
if(cfg_load(CFG_PATH, &g_cfg) != 0){
|
||||
/* First boot: drop a template so FTP edit is the only step */
|
||||
FILE *probe = fopen(CFG_PATH, "rb");
|
||||
if(!probe){
|
||||
cfg_defaults(&g_cfg);
|
||||
if(cfg_save(CFG_PATH, &g_cfg) != 0)
|
||||
log_msg("WARN: template config unwritable; token edits will not persist");
|
||||
else
|
||||
log_msg("created template %s; edit \"token\" then reboot", CFG_PATH);
|
||||
} else fclose(probe);
|
||||
log_msg("config load failed; running on defaults until valid config appears");
|
||||
}
|
||||
if(!g_cfg.enabled){ log_msg("disabled in config; exiting"); health_mark_clean(); log_close(); return 0; }
|
||||
log_set_debug(g_cfg.debug);
|
||||
if(g_cfg.debug) log_dbg("debug logging on (config)");
|
||||
/* Wall-clock correction for Discord timestamps (PSN time sync is
|
||||
* typically blocked on jailbroken consoles). Boot sweep is bounded;
|
||||
* hourly ticks are single-host attempts (never stall heartbeats). */
|
||||
time_sync_all();
|
||||
if(!have_token(&g_cfg)){
|
||||
/* Waiting for configuration is a HEALTHY boot, not a crash:
|
||||
* mark clean so token-less boots never trip safe mode. */
|
||||
health_mark_healthy();
|
||||
log_msg("FATAL: put your Discord user token in %s as \"token\":\"...\"", CFG_PATH);
|
||||
log_close();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Self-update once per boot, before first connect. Never fatal:
|
||||
* staged artifacts take effect on next launch/injection. */
|
||||
if(g_cfg.auto_update && !safe_mode){
|
||||
int ur = updater_check_and_stage();
|
||||
log_msg("updater: %s (local %s)",
|
||||
ur > 0 ? "staged newer build" : ur == 0 ? "already current" : "check failed",
|
||||
ORBISRPC_VERSION);
|
||||
} else if(safe_mode){
|
||||
log_msg("updater: skipped (safe mode)");
|
||||
}
|
||||
|
||||
discord_t dc;
|
||||
int base_poll = g_cfg.poll_interval_s;
|
||||
if(base_poll < 5) base_poll = 5;
|
||||
if(base_poll > 60) base_poll = 60;
|
||||
/* Reconnect policy: exponential backoff with deterministic jitter,
|
||||
* cap 60s for the first 10 consecutive failures, then 10-minute
|
||||
* cadence (quiet persistence, never exit). Success resets. */
|
||||
int backoff = base_poll;
|
||||
int conn_fails = 0;
|
||||
unsigned jctr = 0;
|
||||
/* Health metrics (hourly HEALTH line). */
|
||||
int64_t boot_mono = orbis_mono_s();
|
||||
unsigned n_posts = 0, n_reconnects = 0;
|
||||
int64_t last_health = 0;
|
||||
for(;;){ /* outer: connect cycles with backoff on failure */
|
||||
if(s_stop) break;
|
||||
/* re-read config every cycle so token edits land without a reboot.
|
||||
* On parse failure keep last-good config instead of stale defaults. */
|
||||
{
|
||||
cfg_t next = g_cfg;
|
||||
if(cfg_load(CFG_PATH, &next) == 0){
|
||||
if(next.debug != g_cfg.debug)
|
||||
log_msg("debug logging %s", next.debug ? "on" : "off");
|
||||
g_cfg = next;
|
||||
log_set_debug(g_cfg.debug);
|
||||
}
|
||||
else log_msg("config reload failed; keeping last-good config");
|
||||
}
|
||||
if(!have_token(&g_cfg)){
|
||||
log_msg("no token in config; waiting %ds", backoff);
|
||||
if(sleep_stop(backoff)) break;
|
||||
continue;
|
||||
}
|
||||
int rc = discord_connect(&dc, g_cfg.token);
|
||||
if(rc == -2){
|
||||
/* Rejected token: do NOT exit (that would strand the daemon
|
||||
* until the next manual injection). The per-cycle config
|
||||
* reload picks up a fixed token on its own. */
|
||||
log_msg("WARN: token rejected by gateway (close 4004). "
|
||||
"Fix \"token\" in %s; retrying", CFG_PATH);
|
||||
int wait = reconnect_delay(&conn_fails, base_poll, &jctr);
|
||||
if(sleep_stop(wait)) break;
|
||||
continue;
|
||||
}
|
||||
if(rc != 0){
|
||||
int wait = reconnect_delay(&conn_fails, base_poll, &jctr);
|
||||
log_msg("gateway connect failed (attempt %d); retry in %ds",
|
||||
conn_fails, wait);
|
||||
if(sleep_stop(wait)) break;
|
||||
continue;
|
||||
}
|
||||
if(conn_fails > 0)
|
||||
log_msg("gateway connected after %d failures", conn_fails);
|
||||
conn_fails = 0;
|
||||
n_reconnects++;
|
||||
backoff = g_cfg.poll_interval_s; /* success resets backoff */
|
||||
if(backoff < 5) backoff = 5;
|
||||
if(backoff > 60) backoff = 60;
|
||||
|
||||
/* last/started live OUTSIDE the session loop so the playing timer
|
||||
* survives gateway reconnects (same game keeps its original start).
|
||||
* A reconnect re-posts presence only when needed, timer intact. */
|
||||
static int active = 0;
|
||||
static pres_state_t pres = PS_NONE;
|
||||
static char last[128] = "";
|
||||
static char sess_tid[16] = "";
|
||||
static int64_t started = 0;
|
||||
static int64_t last_tsync = 0;
|
||||
static int home_posted = 0;
|
||||
/* Fresh (re)connect invalidates whatever Discord shows: re-post the
|
||||
* current state (game via need_post below, home via home_posted
|
||||
* reset) so a drop can never leave a stale/blank tile behind. */
|
||||
home_posted = 0;
|
||||
/* Resume window: if the same title vanishes briefly (detection
|
||||
* flicker, quick menu hop) and returns within 10 minutes, the
|
||||
* timer resumes instead of resetting to 0:00. */
|
||||
static char prev_tid[16] = "";
|
||||
static int64_t prev_started = 0, prev_end = 0;
|
||||
/* Cross-restart resume: a previous run's session (saved on every
|
||||
* transition) seeds the resume window, so reboots and updates
|
||||
* keep the timer instead of resetting it. */
|
||||
{
|
||||
static int sess_restored = 0;
|
||||
if(!sess_restored){
|
||||
sess_restored = 1;
|
||||
FILE *sf = fopen("/data/orbisRPC/session.json", "rb");
|
||||
if(sf){
|
||||
fseek(sf, 0, SEEK_END);
|
||||
long ssz = ftell(sf);
|
||||
fseek(sf, 0, SEEK_SET);
|
||||
if(ssz > 0 && ssz < 1024){
|
||||
char *sb = (char*)malloc((size_t)ssz + 1);
|
||||
if(sb && fread(sb, 1, (size_t)ssz, sf) == (size_t)ssz){
|
||||
sb[ssz] = 0;
|
||||
jl_val_t *sr = jl_parse(sb, (size_t)ssz);
|
||||
if(sr && sr->type == JL_OBJECT){
|
||||
const jl_val_t *t = jl_obj_get(sr, "title_id");
|
||||
const jl_val_t *s2 = jl_obj_get(sr, "started");
|
||||
if(t && t->type == JL_STRING && t->str &&
|
||||
s2 && s2->type == JL_NUMBER){
|
||||
/* Validate before trusting: 9-char id,
|
||||
* sane epoch (2020-2100). A corrupt file
|
||||
* must never seed a bogus timer. */
|
||||
int64_t st = (int64_t)s2->num;
|
||||
if(strlen(t->str) == 9 &&
|
||||
st >= 1577836800LL &&
|
||||
st <= 4102444800LL){
|
||||
strncpy(prev_tid, t->str, sizeof prev_tid-1);
|
||||
prev_started = st;
|
||||
/* Wall saved_at can't mix with the
|
||||
* monotonic resume window: start the
|
||||
* window at boot. Same title seen
|
||||
* within minutes of boot resumes. */
|
||||
prev_end = orbis_mono_s();
|
||||
log_msg("session restored: %s (timer may resume)",
|
||||
prev_tid);
|
||||
} else {
|
||||
log_msg("session file failed validation; starting fresh");
|
||||
}
|
||||
}
|
||||
jl_free(sr);
|
||||
}
|
||||
free(sb);
|
||||
} else if(sb) free(sb);
|
||||
}
|
||||
fclose(sf);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Transition debounce: shell flickers during launches, so a new
|
||||
* title (or disappearance) needs consecutive polls (2 at ~1s). */
|
||||
static char cand_title[16] = "";
|
||||
static int cand_hits = 0, miss_hits = 0;
|
||||
int64_t last_poll = 0;
|
||||
int64_t last_alive = 0;
|
||||
static int healthy_marked = 0;
|
||||
/* re-post after every (re)connect so Discord never sticks on stale */
|
||||
int need_post = active && last[0];
|
||||
while(!s_stop){ /* inner: live session, serviced every second */
|
||||
int64_t now = orbis_mono_s();
|
||||
if(now - last_tsync >= 3600){ last_tsync = now; time_sync(); }
|
||||
if(now != last_poll){
|
||||
last_poll = now;
|
||||
if(now - last_alive >= 60){
|
||||
last_alive = now;
|
||||
log_msg("alive: %s", active ? last : "idle");
|
||||
}
|
||||
/* State reconciliation: re-post current presence every
|
||||
* 15 min so a silently desynced tile (dropped update,
|
||||
* stale cache, missed reconnect) heals itself without
|
||||
* any state change. Timer/source values are re-read at
|
||||
* post time, so this never disturbs a live session. */
|
||||
{
|
||||
static int64_t last_reconcile = 0;
|
||||
if(last_reconcile == 0) last_reconcile = now;
|
||||
if(now - last_reconcile >= 900){
|
||||
last_reconcile = now;
|
||||
if(active && last[0]) need_post = 1;
|
||||
else home_posted = 0;
|
||||
log_msg("reconcile: reposting current state");
|
||||
}
|
||||
}
|
||||
/* Hourly health metrics: uptime, posts, reconnects, fails. */
|
||||
if(now - last_health >= 3600){
|
||||
last_health = now;
|
||||
log_msg("HEALTH: %lldh uptime, %u posts, %u reconnects, %d recent fails",
|
||||
(long long)((now - boot_mono) / 3600),
|
||||
n_posts, n_reconnects, conn_fails);
|
||||
}
|
||||
/* A boot that survives HEALTH_STABLE_SECS of runtime was
|
||||
* healthy: clear the unclean-boot marker so only real
|
||||
* crashes count toward safe mode. */
|
||||
if(!healthy_marked && now - boot_mono >= HEALTH_STABLE_SECS){
|
||||
healthy_marked = 1;
|
||||
health_mark_healthy();
|
||||
log_msg("health: boot marked healthy");
|
||||
}
|
||||
char name[128] = "";
|
||||
int scan_unknown = 0;
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
/* Launch/close shortcut: the eboot set changing means the
|
||||
* foreground game changed RIGHT NOW. Reset debounce so the
|
||||
* switch commits within ~2 polls. Deliberately NOT instant:
|
||||
* the title scan needs one poll for fresh atime data, and
|
||||
* committing a stale scan instantly would lock the wrong
|
||||
* title with a full session. */
|
||||
{
|
||||
static int last_eboots = -1;
|
||||
int nboots = detect_eboot_count();
|
||||
if(nboots >= 0 && nboots != last_eboots){
|
||||
if(last_eboots >= 0)
|
||||
log_msg("eboot set %d -> %d; fast-switching",
|
||||
last_eboots, nboots);
|
||||
last_eboots = nboots;
|
||||
cand_title[0] = 0;
|
||||
cand_hits = 0;
|
||||
miss_hits = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if(fixed_game_name){
|
||||
strncpy(name, fixed_game_name, sizeof name-1);
|
||||
name[sizeof name-1] = 0;
|
||||
}else{
|
||||
/* detect_current_game already checks foreground-active
|
||||
* internally; don't double-call it. -2 means the scan
|
||||
* itself failed: hold position, touch nothing. */
|
||||
scan_unknown = (detect_current_game(name, sizeof name, NULL, 0) == -2);
|
||||
if(scan_unknown){
|
||||
log_dbg("detect scan failed; holding");
|
||||
name[0] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if(name[0]){
|
||||
const char *cur_tid = detect_last_titleid();
|
||||
if(!cur_tid) cur_tid = "";
|
||||
if(!strcmp(cur_tid, cand_title)){
|
||||
cand_hits++;
|
||||
} else {
|
||||
strncpy(cand_title, cur_tid, sizeof cand_title-1);
|
||||
cand_title[sizeof cand_title-1] = 0;
|
||||
cand_hits = 1;
|
||||
}
|
||||
miss_hits = 0;
|
||||
if(fixed_game_name || cand_hits >= 2){
|
||||
if(!active || strcmp(cur_tid,sess_tid)!=0){
|
||||
/* Switching away from a live session: bank its time.
|
||||
* (Fresh starts and resumes have nothing to bank;
|
||||
* the clear path already banked ended sessions.) */
|
||||
if(active && sess_tid[0] &&
|
||||
strcmp(cur_tid,sess_tid)!=0)
|
||||
ledger_append(sess_tid, last, started, time_fixed());
|
||||
active = 1;
|
||||
strncpy(sess_tid, cur_tid, sizeof sess_tid-1);
|
||||
strncpy(last, name, sizeof last-1);
|
||||
last[sizeof last-1] = 0;
|
||||
if(!strcmp(cur_tid, prev_tid) &&
|
||||
prev_started > 0 &&
|
||||
now - prev_end < 600){
|
||||
started = prev_started;
|
||||
log_msg("SESSION_RESUMED title=%s (gap %llds, timer kept)",
|
||||
cur_tid[0]?cur_tid:"?", (long long)(now - prev_end));
|
||||
} else {
|
||||
started = time_fixed();
|
||||
}
|
||||
need_post = 1;
|
||||
log_msg("GAME_DETECTED title=%s name=%s", cur_tid[0]?cur_tid:"?", name);
|
||||
art_cache_clear();
|
||||
sess_save(cur_tid, name, started);
|
||||
/* Self-learning map: persist authoritatively resolved
|
||||
* names so later boots resolve instantly, even when
|
||||
* every live source is unreachable. Raw-ID fallbacks
|
||||
* (ok=0) are never learned. */
|
||||
if(cur_tid[0] && detect_last_ok() && cfg_learn(&g_cfg, cur_tid, name)){
|
||||
if(cfg_save(CFG_PATH, &g_cfg) != 0)
|
||||
log_msg("config: learn save failed for %s", cur_tid);
|
||||
else
|
||||
log_msg("config: learned %s", cur_tid);
|
||||
}
|
||||
} else if(strcmp(name,last)!=0){
|
||||
strncpy(last, name, sizeof last-1);
|
||||
last[sizeof last-1] = 0;
|
||||
need_post = 1;
|
||||
}
|
||||
if(need_post){
|
||||
const char *state = g_cfg.presence_state[0] ? g_cfg.presence_state : NULL;
|
||||
const char *tart = detect_last_art();
|
||||
/* Playback queue of one: only clear need_post after
|
||||
* the bytes actually go out. A failed send stays
|
||||
* queued and rides the next tick/reconnect. */
|
||||
int pr = discord_set_presence_ex(&dc, state, last, sess_tid[0]?sess_tid:NULL, g_cfg.application_id, g_cfg.art_base_url, tart, g_cfg.home_art[0]?g_cfg.home_art:NULL, started);
|
||||
if(pr == 0){
|
||||
log_msg("presence: %s", last);
|
||||
pres_set(&pres, PS_GAME);
|
||||
n_posts++;
|
||||
active = 1; need_post = 0;
|
||||
} else {
|
||||
log_msg("presence send failed; will retry");
|
||||
}
|
||||
}
|
||||
}
|
||||
}else if(active && !scan_unknown){
|
||||
cand_title[0] = 0;
|
||||
cand_hits = 0;
|
||||
if(++miss_hits >= 2){
|
||||
discord_clear_presence(&dc);
|
||||
log_msg("presence cleared");
|
||||
pres_set(&pres, PS_NONE);
|
||||
ledger_append(sess_tid, last, started, time_fixed());
|
||||
strncpy(prev_tid, sess_tid, sizeof prev_tid-1);
|
||||
prev_started = started;
|
||||
prev_end = now;
|
||||
remove("/data/orbisRPC/session.json");
|
||||
last[0]=0; sess_tid[0]=0; active=0; started=0;
|
||||
home_posted = 0;
|
||||
}
|
||||
} else if(!home_posted && !scan_unknown){
|
||||
/* Home screen support: no game running. Post a timerless
|
||||
* home presence once (instead of bare online), clear it
|
||||
* the moment a game commits. */
|
||||
const char *state = g_cfg.presence_state[0] ? g_cfg.presence_state : NULL;
|
||||
if(discord_set_presence_ex(&dc, state, "PlayStation 4", "home",
|
||||
g_cfg.application_id, g_cfg.art_base_url,
|
||||
g_cfg.home_art[0] ? g_cfg.home_art : NULL, NULL, 0) == 0){
|
||||
log_msg("presence: home");
|
||||
pres_set(&pres, PS_HOME);
|
||||
n_posts++;
|
||||
home_posted = 1;
|
||||
} else {
|
||||
log_msg("home presence send failed; will retry");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 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){
|
||||
/* Token rejected mid-session: same policy as connect-time
|
||||
* 4004 (never strand the daemon). Drop to the outer loop:
|
||||
* backoff + config reload picks up a fixed token alone. */
|
||||
log_msg("WARN: token rejected (close 4004). Fix %s; retrying", CFG_PATH);
|
||||
ws_close(&dc.ws);
|
||||
pres_set(&pres, PS_NONE);
|
||||
if(sleep_stop(reconnect_delay(&conn_fails, base_poll, &jctr))) break;
|
||||
break;
|
||||
}
|
||||
if(tr == -3){
|
||||
int wait = reconnect_delay(&conn_fails, base_poll, &jctr);
|
||||
log_msg("invalid session; fresh identify (attempt %d) in %ds",
|
||||
conn_fails, wait);
|
||||
ws_close(&dc.ws);
|
||||
pres_set(&pres, PS_NONE);
|
||||
if(sleep_stop(wait)) break;
|
||||
break;
|
||||
}
|
||||
if(tr != 0){
|
||||
int wait = reconnect_delay(&conn_fails, base_poll, &jctr);
|
||||
log_msg("gateway dropped (attempt %d); reconnecting in %ds",
|
||||
conn_fails, wait);
|
||||
ws_close(&dc.ws);
|
||||
pres_set(&pres, PS_NONE);
|
||||
if(sleep_stop(wait)) break;
|
||||
break;
|
||||
}
|
||||
|
||||
for(int i = 0; i < 10 && !s_stop; i++) usleep(100000);
|
||||
}
|
||||
if(s_stop && dc.connected) discord_clear_presence(&dc);
|
||||
ws_close(&dc.ws);
|
||||
}
|
||||
/* Session file already reflects the live session (saved on every
|
||||
* transition), so shutdown is: clear presence (above), release the
|
||||
* lock so a successor starts immediately, close the log. */
|
||||
lock_release();
|
||||
log_close();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/* daemon.h - shared orbisRPC daemon loop (payload ELF and GoldHEN plugin). */
|
||||
#ifndef DAEMON_H
|
||||
#define DAEMON_H
|
||||
/* fixed_game_name != NULL: post presence for that game only (plugin mode).
|
||||
* NULL: poll the foreground app (payload mode).
|
||||
* Returns 0 on clean stop, 1 on missing/invalid configuration, or 2 when
|
||||
* Discord rejects the token with close code 4004. */
|
||||
int daemon_run(const char *fixed_game_name);
|
||||
void daemon_request_stop(void);
|
||||
void daemon_clear_stop(void);
|
||||
int daemon_stop_requested(void); /* 1 when plugin_unload() wants us to exit */
|
||||
#endif
|
||||
@@ -0,0 +1,593 @@
|
||||
/* detect.c
|
||||
* Foreground-game detection on PS4.
|
||||
*
|
||||
* Primary signal: sceShellCoreUtilIsAppLaunched() — resolved at runtime via
|
||||
* dlopen/dlsym (libSceShellCoreUtil.sprx). Returns 1 when a user app (game)
|
||||
* is in the foreground vs the home screen. This is the reliable "playing"
|
||||
* indicator; it stays 0 on the home screen so presence clears.
|
||||
*
|
||||
* Fallback (if ShellCoreUtil can't be resolved in the payload context):
|
||||
* sceUserServiceGetForegroundUser >= 0 + most-recent app.xml heuristic.
|
||||
*
|
||||
* Title naming is best-effort:
|
||||
* a) /data/app/<titleid>/app.xml <title> for the most-recently-modified dir
|
||||
* b) app.db tbl_app_static scan (crude byte scan)
|
||||
* c) titleId (CUSAxxxxx) as last resort
|
||||
*
|
||||
* NOTE: mapping the foreground app to its pid/titleId is not exposed by the
|
||||
* public SDK, so the NAME is heuristic. The core signal (a game is in the
|
||||
* foreground) is reliable via ShellCoreUtil.
|
||||
*/
|
||||
#include "detect.h"
|
||||
#include "cfg.h"
|
||||
#include "log.h"
|
||||
#include "sfo.h"
|
||||
#include "tmdb.h"
|
||||
#include "clock.h"
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
/* Payload-SDK build: dlopen/dlsym come from the SDK libc (dlfcn);
|
||||
* there is no UserService here — user_init() below degrades. */
|
||||
#include <dlfcn.h>
|
||||
#include <stdint.h>
|
||||
#else
|
||||
#include <orbis/UserService.h>
|
||||
#include <orbis/libkernel.h>
|
||||
#include <orbis/Sysmodule.h>
|
||||
#endif
|
||||
#include <unistd.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <fcntl.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
#include <sys/types.h>
|
||||
#include <sys/sysctl.h>
|
||||
|
||||
static int is_title_prefix(const char *n);
|
||||
|
||||
/* --- SDK-build foreground/title signals (no ShellCoreUtil/UserService) ---
|
||||
* A launched game shows up as an "eboot.bin" process; its identity comes
|
||||
* from the freshest savedata dir (gameplay writes saves continuously).
|
||||
* Both facts verified live via probes before wiring them in. */
|
||||
/* Count eboot.bin processes. A change means launch/close: callers drop
|
||||
* stale state immediately instead of waiting out debounce windows. */
|
||||
int detect_eboot_count(void){
|
||||
int mib[4] = { 1, 14, 8, 0 };
|
||||
size_t sz = 0;
|
||||
int n = 0;
|
||||
if(sysctl(mib, 4, NULL, &sz, NULL, 0) != 0) return -1;
|
||||
static unsigned char buf[256*1024];
|
||||
if(sz > sizeof buf) return -1;
|
||||
if(sysctl(mib, 4, buf, &sz, NULL, 0) != 0) return -1;
|
||||
size_t off = 0;
|
||||
while(off + 4 <= sz){
|
||||
int recsz = *(int *)(buf + off);
|
||||
if(recsz <= 0 || off + (size_t)recsz > sz) break;
|
||||
if(recsz >= 479 && !memcmp(buf + off + 447, "eboot.bin", 10))
|
||||
n++;
|
||||
off += (size_t)recsz;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
static int proc_has_eboot(void){
|
||||
int mib[4] = { 1, 14, 8, 0 };
|
||||
size_t sz = 0;
|
||||
if(sysctl(mib, 4, NULL, &sz, NULL, 0) != 0) return -1;
|
||||
static unsigned char buf[256*1024];
|
||||
if(sz > sizeof buf) return -1;
|
||||
if(sysctl(mib, 4, buf, &sz, NULL, 0) != 0) return -1;
|
||||
size_t off = 0;
|
||||
while(off + 4 <= sz){
|
||||
int recsz = *(int *)(buf + off);
|
||||
if(recsz <= 0 || off + (size_t)recsz > sz) return -1;
|
||||
if(recsz >= 479 && !memcmp(buf + off + 447, "eboot.bin", 10))
|
||||
return 1;
|
||||
off += (size_t)recsz;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Sandbox mounts: /mnt/sandbox/<TITLE>_000 exists exactly while that
|
||||
* title's game process lives. This is authoritative foreground identity
|
||||
* straight from the OS — no heuristics, no races, no sync pollution.
|
||||
* Verified live: only the running game's mount is listed. */
|
||||
static int scan_sandbox_mount(char *out, size_t cap){
|
||||
if(!out || cap < 10) return -1;
|
||||
DIR *d = opendir("/mnt/sandbox");
|
||||
if(!d) return -1;
|
||||
struct dirent *e;
|
||||
int found = 0;
|
||||
char best[16] = "";
|
||||
while((e = readdir(d))){
|
||||
const char *n = e->d_name;
|
||||
if(strlen(n) != 13) continue; /* TITLEID_000 */
|
||||
if(n[9] != '_' || n[10] != '0' || n[11] != '0' || n[12] != '0') continue;
|
||||
char tid[16];
|
||||
memcpy(tid, n, 9);
|
||||
tid[9] = 0;
|
||||
if(!is_title_prefix(tid)) continue;
|
||||
if(!found){
|
||||
strncpy(best, tid, sizeof best - 1);
|
||||
found = 1;
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
if(!found) return -1;
|
||||
strncpy(out, best, cap - 1);
|
||||
out[cap - 1] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static long scan_newest_save(char *out, size_t cap){ static const char *users[] = { "1898cd02", "1898cd03", NULL };
|
||||
/* user dirs vary per console; probe the known ones plus a scan of
|
||||
* /user/home for anything looking like a user id dir. */
|
||||
char udirs[8][32];
|
||||
int ndirs = 0;
|
||||
DIR *hd = opendir("/user/home");
|
||||
if(hd){
|
||||
struct dirent *e;
|
||||
while((e = readdir(hd)) && ndirs < 8){
|
||||
if(e->d_name[0] == '.') continue;
|
||||
strncpy(udirs[ndirs], e->d_name, 31);
|
||||
udirs[ndirs][31] = 0;
|
||||
ndirs++;
|
||||
}
|
||||
closedir(hd);
|
||||
}
|
||||
for(int i = 0; users[i] && ndirs < 8; i++){
|
||||
int dup = 0;
|
||||
for(int k = 0; k < ndirs; k++) if(!strcmp(udirs[k], users[i])) dup = 1;
|
||||
if(!dup){ strncpy(udirs[ndirs], users[i], 31); udirs[ndirs][31] = 0; ndirs++; }
|
||||
}
|
||||
long best = -1;
|
||||
out[0] = 0;
|
||||
const char *best_src = "none";
|
||||
/* Per-title activity = max(savedata writes, app.pkg access time).
|
||||
* app.pkg atime is the sharper signal: the running game streams its
|
||||
* own package continuously, while save mtimes get bulk-touched by
|
||||
* cloud sync (proven: all 28 titles sharing one mtime). atime wins
|
||||
* ties because only gameplay advances it. */
|
||||
for(int u = 0; u < ndirs; u++){
|
||||
char spath[96];
|
||||
snprintf(spath, sizeof spath, "/user/home/%s/savedata", udirs[u]);
|
||||
DIR *sd = opendir(spath);
|
||||
if(!sd) continue;
|
||||
struct dirent *e;
|
||||
while((e = readdir(sd))){
|
||||
if(e->d_name[0] == '.') continue;
|
||||
if(strlen(e->d_name) != 9 || !is_title_prefix(e->d_name)) continue;
|
||||
char tp[160];
|
||||
snprintf(tp, sizeof tp, "%s/%s", spath, e->d_name);
|
||||
/* newest write inside the title dir wins */
|
||||
DIR *td = opendir(tp);
|
||||
long tb = -1;
|
||||
if(td){
|
||||
struct dirent *f;
|
||||
while((f = readdir(td))){
|
||||
if(f->d_name[0] == '.') continue;
|
||||
/* d_name can be up to 255 chars; tp already holds up
|
||||
* to ~150 — skip overlong names instead of overflowing. */
|
||||
if(strlen(f->d_name) > 64) continue;
|
||||
char fp[256];
|
||||
int wn = snprintf(fp, sizeof fp, "%s/%s", tp, f->d_name);
|
||||
if(wn <= 0 || (size_t)wn >= sizeof fp) continue;
|
||||
struct stat fs;
|
||||
if(stat(fp, &fs) == 0 && fs.st_mtime > tb) tb = fs.st_mtime;
|
||||
}
|
||||
closedir(td);
|
||||
}
|
||||
if(tb < 0){
|
||||
struct stat ds;
|
||||
if(stat(tp, &ds) == 0) tb = ds.st_mtime;
|
||||
}
|
||||
if(tb > best){ best = tb; strncpy(out, e->d_name, cap-1); out[cap-1] = 0; best_src = "save"; }
|
||||
}
|
||||
closedir(sd);
|
||||
}
|
||||
/* Second signal: /user/app/<TITLE> dir mtime. Launches touch the app
|
||||
* dir even when the game hasn't saved yet (the exact hole that showed
|
||||
* a stale title for a freshly launched game), and it covers titles
|
||||
* with no savedata at all. */
|
||||
{
|
||||
DIR *ad = opendir("/user/app");
|
||||
if(ad){
|
||||
struct dirent *e;
|
||||
while((e = readdir(ad))){
|
||||
if(strlen(e->d_name) != 9 || !is_title_prefix(e->d_name)) continue;
|
||||
char ap[64];
|
||||
snprintf(ap, sizeof ap, "/user/app/%s", e->d_name);
|
||||
struct stat st;
|
||||
if(stat(ap, &st) == 0 && st.st_mtime > best){
|
||||
best = st.st_mtime;
|
||||
strncpy(out, e->d_name, cap-1);
|
||||
out[cap-1] = 0;
|
||||
best_src = "appdir";
|
||||
}
|
||||
}
|
||||
closedir(ad);
|
||||
}
|
||||
}
|
||||
/* Third signal (strongest): app.pkg ACCESS time. The running game
|
||||
* streams its own package, so its atime is the freshest on the box;
|
||||
* cloud sync touches mtimes, never atimes. Verified live: the
|
||||
* foreground title's atime beats every idle title by days. */
|
||||
{
|
||||
DIR *ad = opendir("/user/app");
|
||||
if(ad){
|
||||
struct dirent *e;
|
||||
while((e = readdir(ad))){
|
||||
if(strlen(e->d_name) != 9 || !is_title_prefix(e->d_name)) continue;
|
||||
char pp[96];
|
||||
int wn = snprintf(pp, sizeof pp, "/user/app/%s/app.pkg",
|
||||
e->d_name);
|
||||
if(wn <= 0 || (size_t)wn >= sizeof pp) continue;
|
||||
struct stat st;
|
||||
if(stat(pp, &st) == 0 && (long)st.st_atime > best){
|
||||
best = (long)st.st_atime;
|
||||
strncpy(out, e->d_name, cap-1);
|
||||
out[cap-1] = 0;
|
||||
best_src = "pkg-atime";
|
||||
}
|
||||
}
|
||||
closedir(ad);
|
||||
}
|
||||
}
|
||||
if(out[0]) log_dbg("title scan: %s via %s", out, best_src);
|
||||
return out[0] ? 0 : -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
static int s_user_inited = 0;
|
||||
static int s_user_ok = 0;
|
||||
static int user_init(void){
|
||||
if(s_user_inited) return s_user_ok ? 0 : -1;
|
||||
s_user_inited = 1;
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
/* No UserService in payload-SDK builds: ShellCoreUtil (dlopen) is the
|
||||
* only foreground signal; without it we report inactive (fail closed). */
|
||||
log_msg("UserService unavailable in SDK build; ShellCoreUtil only");
|
||||
return -1;
|
||||
#else
|
||||
/* UserService is an external module -> load via internal id */
|
||||
uint32_t r = sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_USER_SERVICE);
|
||||
if(r != 0){ int32_t ir=(int32_t)r; log_msg("load UserService fail %d", ir); return -1; }
|
||||
int32_t rc = sceUserServiceInitialize(NULL);
|
||||
if(rc != 0){ log_msg("UserService init fail %d", rc); return -1; }
|
||||
s_user_ok = 1;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* --- ShellCoreUtil runtime resolution ------------------------------- */
|
||||
/* PS4 SDK has no <dlfcn.h>; libkernel exports dlopen/dlsym directly. */
|
||||
extern void *dlopen(const char *filename, int flags);
|
||||
extern void *dlsym(void *handle, const char *symbol);
|
||||
typedef int (*shellcore_isapplaunched_fn)(void);
|
||||
static shellcore_isapplaunched_fn s_is_app_launched = NULL;
|
||||
static int s_scu_tried = 0;
|
||||
|
||||
static int scu_init(void){
|
||||
if(s_scu_tried) return s_is_app_launched != NULL;
|
||||
s_scu_tried = 1;
|
||||
void *h = dlopen("libSceShellCoreUtil.sprx", 0);
|
||||
if(!h){ log_msg("ShellCoreUtil unavailable (dlopen fail); using foreground-user fallback"); return 0; }
|
||||
s_is_app_launched = (shellcore_isapplaunched_fn)(uintptr_t)dlsym(h, "sceShellCoreUtilIsAppLaunched");
|
||||
log_msg("ShellCoreUtil IsAppLaunched %s", s_is_app_launched ? "resolved" : "unavailable (dlsym fail)");
|
||||
return s_is_app_launched != NULL;
|
||||
}
|
||||
|
||||
/* --- foreground-active: the core "a game is running" signal ---------- */
|
||||
int detect_foreground_active(void){
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
/* Spawned processes see no ShellCoreUtil/UserService; the eboot.bin
|
||||
* process itself is the signal (system has no other eboot). */
|
||||
return proc_has_eboot();
|
||||
#else
|
||||
if(scu_init() && s_is_app_launched){
|
||||
int on = s_is_app_launched();
|
||||
return (on != 0) ? 1 : 0; /* 0 when sitting on the home screen */
|
||||
}
|
||||
#endif
|
||||
/* fallback: foreground user exists. If UserService itself failed,
|
||||
* report inactive instead of guessing "playing". */
|
||||
if(user_init() != 0) return 0;
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
/* No UserService API in SDK builds (user_init always fails there,
|
||||
* so this is unreachable); fail closed regardless. */
|
||||
return 0;
|
||||
#else
|
||||
int32_t fg = -1;
|
||||
int32_t rc = sceUserServiceGetForegroundUser(&fg);
|
||||
if(rc != 0){ log_msg("GetForegroundUser err %d", rc); return 0; }
|
||||
return (fg >= 0) ? 1 : 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* --- title naming ---------------------------------------------------- */
|
||||
static int is_title_prefix(const char *n){
|
||||
/* CUSA (PS4) + PPSA (PS5-backport) + EU/JP/indie variants */
|
||||
return strncmp(n,"CUSA",4)==0 || strncmp(n,"PPSA",4)==0 ||
|
||||
strncmp(n,"PCSE",4)==0 || strncmp(n,"PCSB",4)==0 ||
|
||||
strncmp(n,"PCSG",4)==0 || strncmp(n,"EPSA",4)==0;
|
||||
}
|
||||
/* last resolved titleId (for Discord asset key); valid after a successful
|
||||
* detect_current_game / detect_name_for_title. */
|
||||
static char s_last_titleid[16] = "";
|
||||
static char s_last_art[256] = "";
|
||||
/* Last resolved title: same title in a row reuses its name/art with zero
|
||||
* I/O. Any title change resolves fresh — no cross-title cache exists, so
|
||||
* stale identities are impossible by construction. */
|
||||
static char s_rs_tid[16] = "";
|
||||
static char s_rs_name[128] = "";
|
||||
static char s_rs_art[256] = "";
|
||||
/* Raw-ID (unresolved) entries are retried, not frozen: a transient miss
|
||||
* (slow I/O at launch) must not lock the raw ID in forever. Successful
|
||||
* resolves reuse indefinitely; raw fallbacks re-resolve after 5 min. */
|
||||
static int s_rs_ok = 0;
|
||||
static int64_t s_rs_at = 0;
|
||||
static int resolve_reuse(const char *ti, char *out_name, size_t cap){
|
||||
if(!ti || !ti[0] || strcmp(ti, s_rs_tid) != 0 || !s_rs_name[0]) return 0;
|
||||
if(!s_rs_ok && orbis_mono_s() - s_rs_at > 300) return 0;
|
||||
strncpy(out_name, s_rs_name, cap - 1);
|
||||
out_name[cap - 1] = 0;
|
||||
strncpy(s_last_art, s_rs_art, sizeof s_last_art - 1);
|
||||
s_last_art[sizeof s_last_art - 1] = 0;
|
||||
return 1;
|
||||
}
|
||||
static void resolve_remember(const char *ti, const char *name, const char *art, int ok){
|
||||
if(!ti || !name) return;
|
||||
s_rs_ok = ok;
|
||||
s_rs_at = orbis_mono_s();
|
||||
strncpy(s_rs_tid, ti, sizeof s_rs_tid - 1);
|
||||
s_rs_tid[sizeof s_rs_tid - 1] = 0;
|
||||
strncpy(s_rs_name, name, sizeof s_rs_name - 1);
|
||||
s_rs_name[sizeof s_rs_name - 1] = 0;
|
||||
if(art){
|
||||
strncpy(s_rs_art, art, sizeof s_rs_art - 1);
|
||||
s_rs_art[sizeof s_rs_art - 1] = 0;
|
||||
} else s_rs_art[0] = 0;
|
||||
}
|
||||
/* Media apps post Watching/Listening instead of Playing. IDs verified
|
||||
* against Sony TMDB (names resolve there too). */
|
||||
int detect_media_type(const char *title_id){
|
||||
static const struct { const char *id; int type; } media[] = {
|
||||
{ "CUSA00127", 3 }, /* Netflix -> Watching */
|
||||
{ "CUSA01015", 3 }, /* YouTube -> Watching */
|
||||
};
|
||||
if(!title_id) return 0;
|
||||
for(unsigned i = 0; i < sizeof media/sizeof media[0]; i++){
|
||||
if(!strcmp(title_id, media[i].id)) return media[i].type;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *detect_last_titleid(void){ return s_last_titleid[0] ? s_last_titleid : NULL; }
|
||||
int detect_last_ok(void){ return s_rs_ok; }
|
||||
const char *detect_last_art(void){ return s_last_art[0] ? s_last_art : NULL; }
|
||||
static void remember_titleid(const char *ti){
|
||||
if(!ti) return;
|
||||
strncpy(s_last_titleid, ti, sizeof s_last_titleid - 1);
|
||||
s_last_titleid[sizeof s_last_titleid - 1] = 0;
|
||||
}
|
||||
static long scan_one_appdir(const char *base, char *out, size_t cap, long best){
|
||||
if(!base || !out || cap < 2) return best;
|
||||
DIR *d = opendir(base);
|
||||
if(!d) return best;
|
||||
struct dirent *e;
|
||||
char path[256]; size_t plen;
|
||||
while((e=readdir(d))){
|
||||
if(e->d_name[0]=='.') continue;
|
||||
size_t l=strlen(e->d_name);
|
||||
if(l!=9 || !is_title_prefix(e->d_name)) continue;
|
||||
plen=snprintf(path,sizeof path,"%s/%s/app.xml",base,e->d_name);
|
||||
if(plen>=sizeof path) continue;
|
||||
struct stat st2;
|
||||
if(stat(path,&st2)==0){
|
||||
const char *ti = e->d_name;
|
||||
if(st2.st_mtime > best || best<0){ best=st2.st_mtime; if(cap>1)strncpy(out,ti,cap-1); out[cap-1]=0; }
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
return best;
|
||||
}
|
||||
#ifndef ORBISRPC_SDK_PAYLOAD
|
||||
static long scan_recent_titleid(char *out, size_t cap){
|
||||
out[0]=0;
|
||||
long best = scan_one_appdir("/user/app", out, cap, -1);
|
||||
best = scan_one_appdir("/data/app", out, cap, best);
|
||||
return (out[0])? 0 : -1;
|
||||
}
|
||||
#endif /* scan_recent_titleid unused in SDK builds */
|
||||
|
||||
/* Best name on the box: /user/appmeta/<id>/pronunciation.xml holds the
|
||||
* display title in its first <text> element (works for every game that
|
||||
* ships speech data, e.g. Terraria). Small file, single read. */
|
||||
static int pronunc_title(const char *titleId, char *out, size_t cap){
|
||||
char path[256]; snprintf(path,sizeof path,"/user/appmeta/%s/pronunciation.xml",titleId);
|
||||
int fd=open(path,O_RDONLY);
|
||||
if(fd<0){ log_msg("appmeta open fail %s err=%d", path, errno); return -1; }
|
||||
char buf[2048]; ssize_t n=read(fd,buf,sizeof buf-1); close(fd);
|
||||
if(n<=0) return -1; buf[n]=0;
|
||||
const char *p=strstr(buf,"<text>");
|
||||
if(!p) return -1;
|
||||
p+=6;
|
||||
while(*p==' '||*p=='\t'||*p=='\r'||*p=='\n') p++;
|
||||
size_t i=0;
|
||||
while(*p && *p!='<' && *p!='\n' && *p!='\r' && i<cap-1){ out[i++]=*p++; }
|
||||
out[i]=0;
|
||||
while(i>0 && (out[i-1]==' '||out[i-1]=='\t')) out[--i]=0;
|
||||
return (i>1)?0:-1;
|
||||
}
|
||||
|
||||
/* Game-process-safe TITLE read: the running game's own param.sfo via the
|
||||
* app0 mount plus on-disc sce_sys copies. Small single read, works on ANY
|
||||
* console with zero setup. Returns 0 on success. */
|
||||
static int sfo_file_title(const char *titleId, char *out, size_t cap){
|
||||
char path[256];
|
||||
/* Running game's own sandbox mount first: the payload process can
|
||||
* already list /mnt/sandbox (that is where the title ID comes from),
|
||||
* and the live mount carries the game's own sce_sys/param.sfo with
|
||||
* its TITLE field. Fails soft when untraversable. */
|
||||
if(titleId && titleId[0]){
|
||||
snprintf(path, sizeof path, "/mnt/sandbox/%s_000/sce_sys/param.sfo", titleId);
|
||||
int sfd = open(path, O_RDONLY);
|
||||
if(sfd >= 0){
|
||||
unsigned char buf[4096];
|
||||
ssize_t n = read(sfd, buf, sizeof buf);
|
||||
close(sfd);
|
||||
if(n > 0 && sfo_title(buf, (size_t)n, out, cap) == 0) return 0;
|
||||
}
|
||||
}
|
||||
const char *fixed[] = { "app0/sce_sys/param.sfo", "/app0/sce_sys/param.sfo", NULL };
|
||||
for(int i = 0; fixed[i]; i++){
|
||||
int fd = open(fixed[i], O_RDONLY);
|
||||
if(fd < 0) continue;
|
||||
unsigned char buf[4096];
|
||||
ssize_t n = read(fd, buf, sizeof buf);
|
||||
close(fd);
|
||||
if(n > 0 && sfo_title(buf, (size_t)n, out, cap) == 0) return 0;
|
||||
}
|
||||
if(titleId && titleId[0]){
|
||||
const char *bases[] = { "/user/app", "/data/app", NULL };
|
||||
for(int b = 0; bases[b]; b++){
|
||||
snprintf(path, sizeof path, "%s/%s/sce_sys/param.sfo", bases[b], titleId);
|
||||
int fd = open(path, O_RDONLY);
|
||||
if(fd < 0) continue;
|
||||
unsigned char buf[4096];
|
||||
ssize_t n = read(fd, buf, sizeof buf);
|
||||
close(fd);
|
||||
if(n > 0 && sfo_title(buf, (size_t)n, out, cap) == 0) return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int appxml_title(const char *titleId, char *out, size_t cap){
|
||||
const char *bases[] = { "/user/app", "/data/app", NULL };
|
||||
for(int b=0; bases[b]; b++){
|
||||
char path[256]; snprintf(path,sizeof path,"%s/%s/app.xml",bases[b],titleId);
|
||||
int fd=open(path,O_RDONLY); if(fd<0) continue;
|
||||
char buf[640]; ssize_t n=read(fd,buf,sizeof buf-1); close(fd);
|
||||
if(n<=0) continue; buf[n]=0;
|
||||
char *p=strstr(buf,"<title>");
|
||||
if(!p) p=strstr(buf,"titleName");
|
||||
if(!p) continue;
|
||||
p = strchr(p, '>');
|
||||
if(!p) continue;
|
||||
p++;
|
||||
while(*p==' '||*p=='\t'||*p=='\r'||*p=='\n') p++; /* trim leading ws */
|
||||
size_t i=0;
|
||||
while(*p && *p!='<' && *p!='\n' && *p!='\r' && i<cap-1){ out[i++]=*p++; }
|
||||
out[i]=0;
|
||||
while(i>0 && (out[i-1]==' '||out[i-1]=='\t')) out[--i]=0; /* trim trailing */
|
||||
if(i>0) return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* NOTE: an earlier revision byte-scanned app.db for titles. Removed:
|
||||
* the packed record layout makes the title/contentId boundary ambiguous
|
||||
* and the scanner returned wrong names (worse than raw IDs). Exact
|
||||
* sources above plus Sony TMDB cover every case instead. */
|
||||
|
||||
int detect_current_game(char *out_name, size_t cap, char *out_path, size_t p_cap){
|
||||
if(!out_name || cap==0) return -1;
|
||||
out_name[0] = 0;
|
||||
if(out_path && p_cap) out_path[0] = 0;
|
||||
{
|
||||
/* Tri-state foreground: 1 game, 0 none, -1 scan failed (unknown).
|
||||
* Unknown must NOT count as disappearance — it means "keep whatever
|
||||
* we had", never a transition. */
|
||||
int fg = detect_foreground_active();
|
||||
if(fg < 0) return -2;
|
||||
if(!fg) return -1;
|
||||
}
|
||||
char titleId[16]=""; int named=0, have_tid=0;
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
/* Sandbox mount first: authoritative OS-level identity. Save-scan
|
||||
* stays as fallback (its mtimes get bulk-touched by cloud sync). */
|
||||
if(scan_sandbox_mount(titleId, sizeof titleId) == 0){
|
||||
have_tid = 1;
|
||||
remember_titleid(titleId);
|
||||
s_last_art[0] = 0;
|
||||
} else if(scan_newest_save(titleId,sizeof titleId)==0){
|
||||
have_tid = 1;
|
||||
remember_titleid(titleId);
|
||||
s_last_art[0] = 0;
|
||||
} else return -1;
|
||||
#else
|
||||
have_tid = (scan_recent_titleid(titleId,sizeof titleId)==0);
|
||||
if(have_tid){
|
||||
remember_titleid(titleId);
|
||||
s_last_art[0] = 0;
|
||||
}
|
||||
#endif
|
||||
if(have_tid){
|
||||
remember_titleid(titleId);
|
||||
s_last_art[0] = 0;
|
||||
if(resolve_reuse(titleId, out_name, cap)){
|
||||
log_dbg("name: %s via cache", out_name);
|
||||
named = 1;
|
||||
}
|
||||
/* cheap, game-process-safe sources first; Sony TMDB (network)
|
||||
* resolves anything local sources miss, on any console. */
|
||||
if(!named && cfg_title(&g_cfg, titleId, out_name, cap)==0){ named=1; log_msg("name: %s via config", out_name); }
|
||||
/* System app.db: the authoritative on-box title registry (SQLite,
|
||||
* read-only). Covers disc + digital where per-file sources miss. */
|
||||
/* appdb_title(titleId, out_name, cap); */
|
||||
if(!named && pronunc_title(titleId, out_name, cap)==0){ named=1; log_msg("name: %s via appmeta", out_name); }
|
||||
if(!named){ if(sfo_file_title(titleId, out_name, cap)==0){ named=1; log_msg("name: %s via sfo", out_name); } }
|
||||
if(!named){ if(appxml_title(titleId, out_name, cap)==0){ named=1; log_msg("name: %s via appxml", out_name); } }
|
||||
if(!named){
|
||||
char art[256] = "";
|
||||
if(tmdb_resolve(titleId, out_name, cap, art, sizeof art)==0){
|
||||
named = 1;
|
||||
strncpy(s_last_art, art, sizeof s_last_art-1);
|
||||
} else s_last_art[0] = 0;
|
||||
}
|
||||
if(!named){ strncpy(out_name, titleId, cap-1); out_name[cap-1]=0; }
|
||||
resolve_remember(titleId, out_name, s_last_art, named);
|
||||
}else{
|
||||
remember_titleid("");
|
||||
s_last_art[0] = 0;
|
||||
strncpy(out_name, "(unknown game)", cap-1); out_name[cap-1]=0;
|
||||
}
|
||||
if(out_path){ snprintf(out_path, p_cap, "/data/orbisRPC/.lastgame/%s", titleId[0]?titleId:"unknown"); }
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Resolve a display name for a KNOWN title id (plugin mode: the plugin is
|
||||
* loaded into the game process and knows the titleid from the GoldHEN SDK,
|
||||
* so we skip the foreground-app heuristics entirely). */
|
||||
int detect_name_for_title(const char *titleId, char *out_name, size_t cap){
|
||||
if(!titleId || !titleId[0] || !out_name || cap==0) return -1;
|
||||
remember_titleid(titleId);
|
||||
s_last_art[0] = 0;
|
||||
if(resolve_reuse(titleId, out_name, cap)){
|
||||
log_dbg("name: %s via cache", out_name);
|
||||
return 0;
|
||||
}
|
||||
/* Game-process-safe only: small reads plus one bounded network
|
||||
* lookup; no multi-megabyte scans anywhere in this codebase. */
|
||||
if(cfg_title(&g_cfg, titleId, out_name, cap)==0){ log_msg("name: %s via config", out_name); resolve_remember(titleId, out_name, "", 1); return 0; }
|
||||
/* appdb_title(titleId, out_name, cap); */ resolve_remember(titleId, out_name, "", 1); return 0;
|
||||
if(pronunc_title(titleId, out_name, cap)==0){ log_msg("name: %s via appmeta", out_name); resolve_remember(titleId, out_name, "", 1); return 0; }
|
||||
else log_msg("name: appmeta miss for %s", titleId);
|
||||
if(sfo_file_title(titleId, out_name, cap)==0){ log_msg("name: %s via sfo", out_name); resolve_remember(titleId, out_name, "", 1); return 0; }
|
||||
if(appxml_title(titleId, out_name, cap)==0){ log_msg("name: %s via appxml", out_name); resolve_remember(titleId, out_name, "", 1); return 0; }
|
||||
{
|
||||
char art[256] = "";
|
||||
if(tmdb_resolve(titleId, out_name, cap, art, sizeof art)==0){
|
||||
strncpy(s_last_art, art, sizeof s_last_art-1);
|
||||
resolve_remember(titleId, out_name, art, 1);
|
||||
return 0; /* tmdb_resolve already logged */
|
||||
}
|
||||
s_last_art[0] = 0;
|
||||
}
|
||||
strncpy(out_name, titleId, cap-1); out_name[cap-1]=0;
|
||||
resolve_remember(titleId, out_name, "", 0);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/* detect.h - find current foreground game title + app cache dir. */
|
||||
#ifndef DETECT_H
|
||||
#define DETECT_H
|
||||
#include <stddef.h>
|
||||
/* Returns 0 and writes a display name when a foreground game is found.
|
||||
* Returns -1 when no game is active or the arguments are invalid.
|
||||
* out_path is optional and receives the per-title cache path when provided. */
|
||||
int detect_current_game(char *out_name, size_t cap, char *out_path, size_t p_cap);
|
||||
/* Returns 1 if a foreground user app is running, 0 if definitely none,
|
||||
* -1 if the scan itself failed (unknown: keep previous state). */
|
||||
int detect_foreground_active(void);
|
||||
/* Number of eboot.bin processes (launch/close churn detector). -1 unknown.
|
||||
* Payload-SDK builds only (needs raw sysctl visibility). */
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
int detect_eboot_count(void);
|
||||
#endif
|
||||
/* Resolve a display name for a KNOWN title id (plugin mode). */
|
||||
int detect_name_for_title(const char *titleId, char *out_name, size_t cap);
|
||||
/* Last resolved titleId (e.g. "CUSA00740") or NULL if none yet.
|
||||
* Used as the Discord asset key so icons follow automatically. */
|
||||
const char *detect_last_titleid(void);
|
||||
/* 1 when the last resolve produced a real name (0 = raw-ID fallback). */
|
||||
int detect_last_ok(void);/* Last resolved artwork URL (Sony CDN via TMDB, or empty). */
|
||||
const char *detect_last_art(void);
|
||||
/* Media type for presence: 0 Playing (games), 2 Listening, 3 Watching.
|
||||
* Only known media apps map; everything else is 0. Extend freely. */
|
||||
int detect_media_type(const char *title_id);
|
||||
#endif
|
||||
@@ -0,0 +1,491 @@
|
||||
/* 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 "clock.h"
|
||||
#include "b64.h"
|
||||
#include "log.h"
|
||||
#include "art.h"
|
||||
#include "jsonlite.h"
|
||||
#include "detect.h"
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
|
||||
#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];
|
||||
memset(b, 0, sizeof b);
|
||||
int fd=open("/dev/urandom",O_RDONLY);
|
||||
if(fd>=0){
|
||||
size_t got = 0;
|
||||
while(got < sizeof b){
|
||||
long r = read(fd, (char *)b + got, sizeof b - got);
|
||||
if(r <= 0) break;
|
||||
got += (size_t)r;
|
||||
}
|
||||
close(fd);
|
||||
if(got < sizeof b){
|
||||
/* short read: mix time in rather than leaving stack bytes */
|
||||
uint32_t t = (uint32_t)time(NULL);
|
||||
for(size_t i = got; i < sizeof b; i++, t = t*1664525u + 1013904223u)
|
||||
b[i] ^= (unsigned char)(t >> 24);
|
||||
}
|
||||
}
|
||||
else { for(int i=0;i<16;i++) b[i]=(unsigned char)(time(NULL)+i*7); }
|
||||
b64_encode(b,16,out);
|
||||
}
|
||||
|
||||
/* Gateway events live INSIDE the JSON text payload ("op":N) — not in the
|
||||
* WebSocket frame header (1=text, 8=close, 9=ping). Scan top-level keys
|
||||
* with string-awareness so a nested `"op":` inside a string value can't
|
||||
* spoof the event type. Works on truncated buffers too (op/s/t sit at the
|
||||
* payload prefix, while the megabyte-long "d" blob may be cut off). */
|
||||
static const char *top_val(const char *json, size_t len, const char *key){
|
||||
size_t kl = strlen(key);
|
||||
int depth = 0, instr = 0, esc = 0;
|
||||
for(size_t i = 0; i < len; i++){
|
||||
char c = json[i];
|
||||
if(instr){
|
||||
if(esc) esc = 0;
|
||||
else if(c == '\\') esc = 1;
|
||||
else if(c == '"') instr = 0;
|
||||
continue;
|
||||
}
|
||||
if(c == '"'){
|
||||
/* possible key: must be at depth 1 followed by optional ws + ':' */
|
||||
size_t j = i + 1, k = 0;
|
||||
while(j < len && k < kl && json[j] == key[k]){ j++; k++; }
|
||||
if(k == kl && j < len && json[j] == '"'){
|
||||
j++;
|
||||
while(j < len && (json[j]==' '||json[j]=='\t')) j++;
|
||||
if(j < len && json[j] == ':'){
|
||||
if(depth == 1) return json + j + 1;
|
||||
/* nested same-name key: skip its string opener below */
|
||||
i = j; continue;
|
||||
}
|
||||
}
|
||||
instr = 1; continue;
|
||||
}
|
||||
if(c == '{' || c == '[') depth++;
|
||||
else if(c == '}' || c == ']') depth--;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
static int gw_op(const char *json, size_t len){
|
||||
const char *v = top_val(json, len, "op");
|
||||
if(!v) return -1;
|
||||
while(v < json+len && (*v==' '||*v=='\t')) v++;
|
||||
if(v >= json+len || *v<'0' || *v>'9') return -1;
|
||||
int op = 0;
|
||||
while(v < json+len && *v>='0' && *v<='9'){ op = op*10 + (*v-'0'); v++; }
|
||||
return op;
|
||||
}
|
||||
static void gw_seq(discord_t *d, const char *json, size_t len){
|
||||
if(!d) return;
|
||||
const char *v = top_val(json, len, "s");
|
||||
if(!v) return;
|
||||
while(v < json+len && (*v==' '||*v=='\t')) v++;
|
||||
if(v >= json+len || *v<'0' || *v>'9') return; /* null or missing: not a seq */
|
||||
int s = 0;
|
||||
while(v < json+len && *v>='0' && *v<='9'){ s = s*10 + (*v-'0'); v++; }
|
||||
d->seq = s;
|
||||
}
|
||||
static int is_ready(const char *json, size_t len){
|
||||
const char *v = top_val(json, len, "t");
|
||||
if(!v) return 0;
|
||||
while(v < json+len && (*v==' '||*v=='\t')) v++;
|
||||
return (size_t)(json+len-v) >= 7 && !memcmp(v, "\"READY\"", 7);
|
||||
}
|
||||
|
||||
/* 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(orbis_mono_s()>deadline) return -4;
|
||||
usleep(50000);
|
||||
}
|
||||
}
|
||||
|
||||
static int send_identify(discord_t *d, const char *token){
|
||||
jl_val_t *root=jl_new_object();
|
||||
if(!root) return -1;
|
||||
jl_obj_set(root,"op",jl_new_int(2));
|
||||
jl_val_t *dd=jl_new_object();
|
||||
if(!dd){ jl_free(root); return -1; }
|
||||
jl_obj_set(dd,"token",jl_new_string(token));
|
||||
jl_val_t *pp=jl_new_object();
|
||||
if(!pp){ jl_free(root); return -1; }
|
||||
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(dd,"properties",pp);
|
||||
jl_val_t *pr=jl_new_object();
|
||||
if(!pr){ jl_free(root); return -1; }
|
||||
jl_obj_set(pr,"status",jl_new_string("online"));
|
||||
jl_obj_set(pr,"activities",jl_new_array());
|
||||
jl_obj_set(pr,"afk",jl_new_bool(0));
|
||||
jl_obj_set(pr,"since",jl_new_null());
|
||||
jl_obj_set(dd,"presence",pr);
|
||||
jl_obj_set(root,"d",dd);
|
||||
char *s=jl_stringify(root); jl_free(root);
|
||||
if(!s) return -1;
|
||||
int rc = ws_send_text(&d->ws,s,strlen(s));
|
||||
free(s);
|
||||
return rc < 0 ? -1 : 0;
|
||||
}
|
||||
|
||||
int discord_connect(discord_t *d, const char *token){
|
||||
if(!d || !token || !token[0]) return -1;
|
||||
memset(d,0,sizeof(*d));
|
||||
strncpy(d->token, token, sizeof d->token-1);
|
||||
d->token[sizeof d->token-1] = 0;
|
||||
char key[64]=""; make_key(key);
|
||||
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=orbis_mono_s();
|
||||
d->last_heartbeat=now; d->last_ack=now;
|
||||
/* HELLO (text frame carrying {"op":10,...}) */
|
||||
char buf[2048]; int op=0,fin=0;
|
||||
int nr=rx_frame(d,buf,sizeof buf,&op,&fin,now+15);
|
||||
if(nr==-4){ log_msg("no HELLO (timeout)"); ws_close(&d->ws); d->connected=0; return -1; }
|
||||
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");
|
||||
if(dd){
|
||||
const jl_val_t *hi=jl_obj_get(dd,"heartbeat_interval");
|
||||
if(hi&&hi->type==JL_NUMBER) d->hb_interval_ms=(int64_t)hi->num;
|
||||
}
|
||||
jl_free(h);
|
||||
}
|
||||
if(!d->hb_interval_ms) d->hb_interval_ms=45000;
|
||||
if(d->hb_interval_ms < 5000){
|
||||
log_msg("discord: hb_interval %lldms suspiciously small; clamping to 5000",
|
||||
(long long)d->hb_interval_ms);
|
||||
d->hb_interval_ms = 5000;
|
||||
}
|
||||
if(send_identify(d, token) < 0){
|
||||
log_msg("discord: identify send failed");
|
||||
ws_close(&d->ws); d->connected=0;
|
||||
return -1;
|
||||
}
|
||||
log_msg("discord: identify sent, hb=%llds",(long long)(d->hb_interval_ms/1000));
|
||||
/* READY confirms the token was accepted. `op` is the WebSocket frame
|
||||
* type; the gateway event is JSON inside — parse it, don't switch on it. */
|
||||
int64_t dl=orbis_mono_s()+20;
|
||||
for(;;){
|
||||
nr=rx_frame(d,buf,sizeof buf,&op,&fin,dl);
|
||||
if(nr==-4){ log_msg("no READY after identify (timeout)"); break; }
|
||||
if(nr<=0){ log_msg("no READY after identify (nr=%d)",nr); break; }
|
||||
if(op==8){
|
||||
if(!fin){ log_msg("fragmented close; reconnecting"); ws_close(&d->ws); d->connected=0; return -1; }
|
||||
if(nr<2){ log_msg("malformed close (len %d); reconnecting", nr); ws_close(&d->ws); d->connected=0; return -1; }
|
||||
unsigned code = (((unsigned char)buf[0]<<8)|((unsigned char)buf[1]));
|
||||
log_msg("gateway closed during auth: %u",code);
|
||||
ws_close(&d->ws); d->connected=0;
|
||||
return code==4004 ? -2 : -1;
|
||||
}
|
||||
if(op==9){ ws_pong(&d->ws); continue; }
|
||||
if(op!=1) continue;
|
||||
int go = gw_op(buf, (size_t)nr);
|
||||
if(go==11){ d->last_ack=orbis_mono_s(); continue; }
|
||||
if(go==0 && is_ready(buf, (size_t)nr)){
|
||||
gw_seq(d, buf, (size_t)nr);
|
||||
log_msg("discord: gateway ready");
|
||||
return 0;
|
||||
}
|
||||
/* other pre-READY events: ignore */
|
||||
}
|
||||
ws_close(&d->ws); d->connected=0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int discord_set_presence(discord_t *d, const char *state, const char *name,
|
||||
const char *application_id, int64_t started_epoch){
|
||||
return discord_set_presence_ex(d, state, name, NULL, application_id, NULL, NULL, NULL, started_epoch);
|
||||
}
|
||||
|
||||
/* Lowercase titleId into an asset-key buffer. Returns key length. */
|
||||
static size_t asset_key(const char *title_id, char *key, size_t cap){
|
||||
size_t ki = 0;
|
||||
if(!title_id || !key || cap == 0) return 0;
|
||||
for(size_t i = 0; title_id[i] && ki < cap-1; i++){
|
||||
char c = title_id[i];
|
||||
if(c>='A'&&c<='Z') c += 'a'-'A';
|
||||
if((c>='a'&&c<='z')||(c>='0'&&c<='9')||c=='_') key[ki++]=c;
|
||||
}
|
||||
key[ki] = 0;
|
||||
return ki;
|
||||
}
|
||||
|
||||
/* Test seam: pure activity-JSON builder (no sockets). token may be ""
|
||||
* to skip the mp: proxy (deterministic offline tests). NULL on OOM. */
|
||||
jl_val_t *discord_build_activity(const char *state, const char *name,
|
||||
const char *title_id, const char *application_id,
|
||||
const char *art_base_url, const char *art_url,
|
||||
const char *small_art_url,
|
||||
int64_t started_epoch, const char *token){
|
||||
if(!name) return NULL;
|
||||
jl_val_t *act=jl_new_object();
|
||||
if(!act) return NULL;
|
||||
jl_obj_set(act,"name",jl_new_string(name?name:""));
|
||||
{
|
||||
/* Media apps (Netflix/YouTube/...) post Watching/Listening. */
|
||||
int atype = 0;
|
||||
if(title_id && title_id[0]){
|
||||
atype = detect_media_type(title_id);
|
||||
if(atype != 0 && atype != 2 && atype != 3) atype = 0;
|
||||
}
|
||||
jl_obj_set(act,"type",jl_new_int(atype)); /* 0 Playing */
|
||||
}
|
||||
if(state&&state[0]) jl_obj_set(act,"state",jl_new_string(state));
|
||||
/* details intentionally mirrors the human-readable name, never the raw
|
||||
* title ID: sending the ID here renders it as a second line under the
|
||||
* game name (and twice when the name itself fell back to the ID).
|
||||
* title_id is still used for media-type lookup and artwork below. */
|
||||
if(started_epoch>0){
|
||||
jl_val_t *ts=jl_new_object();
|
||||
if(!ts){ jl_free(act); return NULL; }
|
||||
if(started_epoch > 0 && started_epoch < 100000000000LL)
|
||||
jl_obj_set(ts,"start",jl_new_int(started_epoch*1000LL)); /* integer 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));
|
||||
else if(title_id&&title_id[0]&&!(art_base_url&&art_base_url[0])&&!(art_url&&art_url[0])){
|
||||
/* No artwork will appear without a shared app or art URL pack:
|
||||
* say so once instead of failing silently every update. */
|
||||
static int art_warned = 0;
|
||||
if(!art_warned){ art_warned = 1;
|
||||
log_msg("art: no application_id or art_base_url; presence sends without artwork"); }
|
||||
}
|
||||
/* Artwork (verified on hardware 2026-09-23): raw https URLs and
|
||||
* dangling keys drop the whole activity. Two working forms, tried in
|
||||
* order: (1) mp: proxy resolved via external-assets for the URL we
|
||||
* have (Sony CDN or art_base_url pack); (2) uploaded asset key. */
|
||||
if(title_id&&!strcmp(title_id,"home")&&application_id&&application_id[0]){
|
||||
/* Idle tile: PlayStation logo. home_art (arrives as art_url):
|
||||
* http(s) URL -> mp: proxy; bare value -> uploaded asset key
|
||||
* used as-is; empty -> <art_base_url>home.png when the pack
|
||||
* carries it. A URL that fails mp: sends NO assets (never a
|
||||
* dangling key: those drop the whole activity). */
|
||||
const char *hsrc = (art_url&&art_url[0]) ? art_url : NULL;
|
||||
char home_pack[320] = "";
|
||||
if(hsrc && strchr(hsrc, '"')) hsrc = NULL; /* config garbage fails
|
||||
soft to the pack default, never a broken JSON POST */
|
||||
if(hsrc && strncmp(hsrc, "http", 4) != 0){
|
||||
for(const char *q = hsrc; *q; q++){
|
||||
char ch = *q;
|
||||
if(!((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_')){
|
||||
hsrc = NULL; /* not a valid asset key: pack default */
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!hsrc && art_base_url&&art_base_url[0]){
|
||||
int n=snprintf(home_pack,sizeof home_pack,"%shome.png",art_base_url);
|
||||
if(n>0 && (size_t)n<sizeof home_pack) hsrc = home_pack;
|
||||
}
|
||||
char hmp[512] = "";
|
||||
const char *himg = NULL;
|
||||
if(hsrc && !strncmp(hsrc,"http",4)){
|
||||
if(token && token[0] && art_resolve_mp(application_id, token, hsrc, hmp, sizeof hmp))
|
||||
himg = hmp;
|
||||
} else if(hsrc){
|
||||
himg = hsrc; /* operator-supplied uploaded key: trusted */
|
||||
}
|
||||
if(himg){
|
||||
jl_val_t *as=jl_new_object();
|
||||
if(as){
|
||||
jl_obj_set(as,"large_image",jl_new_string(himg));
|
||||
jl_obj_set(as,"large_text",jl_new_string(name?name:""));
|
||||
jl_obj_set(act,"assets",as);
|
||||
}
|
||||
}
|
||||
} else if(title_id&&title_id[0]&&application_id&&application_id[0]){
|
||||
char mp[512] = "";
|
||||
const char *src_url = (art_url&&art_url[0]) ? art_url : NULL;
|
||||
char pack_url[320] = "";
|
||||
if(!src_url && art_base_url&&art_base_url[0]&&title_id){
|
||||
char key[16];
|
||||
if(asset_key(title_id, key, sizeof key) >= 4){
|
||||
int n=snprintf(pack_url,sizeof pack_url,"%s%s.png",art_base_url,key);
|
||||
if(n>0 && (size_t)n<sizeof pack_url) src_url = pack_url;
|
||||
}
|
||||
}
|
||||
if(src_url && token && token[0] &&
|
||||
art_resolve_mp(application_id, token, src_url, mp, sizeof mp)){
|
||||
jl_val_t *as=jl_new_object();
|
||||
if(as){
|
||||
jl_obj_set(as,"large_image",jl_new_string(mp));
|
||||
/* hover carries the raw title ID (PC-tool pattern):
|
||||
* visible lines stay clean, ID one hover away. */
|
||||
jl_obj_set(as,"large_text",jl_new_string(title_id));
|
||||
jl_obj_set(act,"assets",as);
|
||||
}
|
||||
} else {
|
||||
char key[16];
|
||||
if(asset_key(title_id, key, sizeof key) >= 4){
|
||||
jl_val_t *as=jl_new_object();
|
||||
if(as){
|
||||
jl_obj_set(as,"large_image",jl_new_string(key));
|
||||
jl_obj_set(as,"large_text",jl_new_string(title_id));
|
||||
jl_obj_set(act,"assets",as);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* System badge (PC-tool pattern): small corner tile, e.g. the
|
||||
* PlayStation logo next to game art. mp: URLs resolve through the
|
||||
* disk-cached proxy; anything unresolvable is omitted, never sent
|
||||
* raw (bad small images drop the whole activity). */
|
||||
if(small_art_url && small_art_url[0]){
|
||||
char smp[512] = "";
|
||||
const char *simg = NULL;
|
||||
if(!strncmp(small_art_url, "mp:", 3)){
|
||||
simg = small_art_url;
|
||||
} else if(!strncmp(small_art_url, "http", 4) && token && token[0] &&
|
||||
art_resolve_mp(application_id, token, small_art_url, smp, sizeof smp)){
|
||||
simg = smp;
|
||||
}
|
||||
if(simg){
|
||||
jl_val_t *as = jl_obj_get(act, "assets");
|
||||
if(as && as->type == JL_OBJECT){
|
||||
jl_obj_set(as, "small_image", jl_new_string(simg));
|
||||
jl_obj_set(as, "small_text", jl_new_string("PlayStation 4"));
|
||||
}
|
||||
}
|
||||
}
|
||||
return act;
|
||||
}
|
||||
|
||||
int discord_set_presence_ex(discord_t *d, const char *state, const char *name,
|
||||
const char *title_id, const char *application_id,
|
||||
const char *art_base_url, const char *art_url,
|
||||
const char *small_art_url,
|
||||
int64_t started_epoch){
|
||||
if(!d || !d->connected || !name) return -1;
|
||||
jl_val_t *act = discord_build_activity(state, name, title_id,
|
||||
application_id, art_base_url, art_url, small_art_url, started_epoch, d->token);
|
||||
if(!act) return -1;
|
||||
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"));
|
||||
/* NOT idle: Gateway expects null, not 0. Sending 0 asserts
|
||||
* "idle since 1970" and can wedge the client timer at 0:00. */
|
||||
jl_obj_set(dd,"since",jl_new_null());
|
||||
jl_obj_set(dd,"afk",jl_new_bool(0));
|
||||
jl_val_t *root=jl_new_object();
|
||||
if(!root){ jl_free(dd); return -1; }
|
||||
jl_obj_set(root,"op",jl_new_int(3));
|
||||
jl_obj_set(root,"d",dd);
|
||||
char *s=jl_stringify(root); jl_free(root);
|
||||
if(!s) return -1;
|
||||
int r=ws_send_text(&d->ws,s,strlen(s)); free(s);
|
||||
return (r>=0)?0:-1;
|
||||
}
|
||||
|
||||
int discord_clear_presence(discord_t *d){
|
||||
if(!d || !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("online")); /* stay visible, just idle */
|
||||
jl_obj_set(dd,"since",jl_new_null());
|
||||
jl_obj_set(dd,"afk",jl_new_bool(0));
|
||||
jl_val_t *root=jl_new_object();
|
||||
if(!root){ jl_free(dd); return -1; }
|
||||
jl_obj_set(root,"op",jl_new_int(3));
|
||||
jl_obj_set(root,"d",dd);
|
||||
char *s=jl_stringify(root); jl_free(root);
|
||||
if(!s) return -1;
|
||||
int r=ws_send_text(&d->ws,s,strlen(s)); free(s);
|
||||
return (r>=0)?0:-1;
|
||||
}
|
||||
|
||||
int discord_tick(discord_t *d){
|
||||
if(!d || !d->connected) return -1;
|
||||
int64_t now=orbis_mono_s();
|
||||
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;
|
||||
}
|
||||
/* 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}");
|
||||
if(ws_send_text(&d->ws,hb,strlen(hb)) < 0){
|
||||
log_msg("heartbeat send failed; reconnecting");
|
||||
d->connected=0;
|
||||
return -1;
|
||||
}
|
||||
d->last_heartbeat=now; d->sent_hb=1;
|
||||
}
|
||||
/* 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; }
|
||||
if(op==8){ /* WS CLOSE: payload starts with a 2-byte big-endian code */
|
||||
if(!fin){ log_msg("fragmented close; reconnecting"); d->connected=0; return -1; }
|
||||
if(nr<2){ log_msg("malformed close (len %d); reconnecting", nr); d->connected=0; return -1; }
|
||||
unsigned code = (((unsigned char)buf[0]<<8)|((unsigned char)buf[1]));
|
||||
log_msg("gateway closed: code=%u",code);
|
||||
d->connected=0;
|
||||
return code==4004 ? -2 : -1;
|
||||
}
|
||||
if(op==9){ ws_pong(&d->ws); continue; } /* WS PING -> PONG */
|
||||
if(op!=1 && op!=0) continue;
|
||||
switch(gw_op(buf, (size_t)nr)){
|
||||
case 0: /* DISPATCH: only the sequence matters to us */
|
||||
gw_seq(d, buf, (size_t)nr);
|
||||
break;
|
||||
case 7: /* RECONNECT requested */
|
||||
log_msg("gateway: reconnect requested");
|
||||
d->connected=0;
|
||||
return -1;
|
||||
case 9: /* INVALID_SESSION: session dead, fresh IDENTIFY needed.
|
||||
* Distinct code so the caller retries promptly instead of
|
||||
* doubling into a long backoff. */
|
||||
log_msg("gateway: invalid session");
|
||||
d->connected=0;
|
||||
return -3;
|
||||
case 11:
|
||||
d->last_ack=now;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/* discord.h - gateway session + presence (user session token). */
|
||||
#ifndef DISCORD_H
|
||||
#define DISCORD_H
|
||||
#include "ws.h"
|
||||
#include "jsonlite.h"
|
||||
typedef struct {
|
||||
ws_t ws;
|
||||
char token[512];
|
||||
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;
|
||||
/* 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 */
|
||||
/* Same + titleId asset key: when application_id is set, sends
|
||||
* assets { large_image: "<lower titleId>", large_text: "<name>" } so the
|
||||
* game icon shows once uploaded under that name in the Discord app.
|
||||
* When art_base_url is set instead, large_image becomes
|
||||
* "<base><lower titleId>.png" (external URL assets, no uploads needed). */
|
||||
int discord_set_presence_ex(discord_t *d, const char *state, const char *name,
|
||||
const char *title_id, const char *application_id,
|
||||
const char *art_base_url, const char *art_url,
|
||||
const char *small_art_url,
|
||||
int64_t started_epoch); /* op 3 */
|
||||
/* Test seam: pure activity-JSON builder (no sockets). token "" skips mp:. */
|
||||
jl_val_t *discord_build_activity(const char *state, const char *name,
|
||||
const char *title_id, const char *application_id,
|
||||
const char *art_base_url, const char *art_url,
|
||||
const char *small_art_url,
|
||||
int64_t started_epoch, const char *token);
|
||||
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; -3 invalid session (retry promptly) */
|
||||
#endif
|
||||
@@ -0,0 +1,182 @@
|
||||
/* health.c - unclean-boot marker + consecutive-crash counter + rollback.
|
||||
*
|
||||
* Correct semantics: a normal reboot must NEVER count as a crash.
|
||||
* boot N: write boot.dirty marker
|
||||
* healthy: (stable runtime) remove marker + reset crash.count to 0
|
||||
* boot N+1: marker present? previous boot died before healthy -> count++.
|
||||
* marker absent? previous boot was clean -> keep count (0).
|
||||
* 3 consecutive unclean boots -> safe mode.
|
||||
*/
|
||||
#include "health.h"
|
||||
#include "updater.h"
|
||||
#include "log.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
|
||||
#ifdef HEALTH_TESTABLE
|
||||
static char g_base[256] = "/data/orbisRPC";
|
||||
void health_set_base(const char *dir){
|
||||
if(dir && dir[0]){
|
||||
snprintf(g_base, sizeof g_base, "%s", dir);
|
||||
g_base[sizeof g_base - 1] = 0;
|
||||
}
|
||||
}
|
||||
static void crash_path(char *out, size_t cap){
|
||||
snprintf(out, cap, "%s/crash.count", g_base);
|
||||
}
|
||||
static void dirty_path(char *out, size_t cap){
|
||||
snprintf(out, cap, "%s/boot.dirty", g_base);
|
||||
}
|
||||
#else
|
||||
static void crash_path(char *out, size_t cap){
|
||||
snprintf(out, cap, "%s", CRASH_PATH);
|
||||
}
|
||||
static void dirty_path(char *out, size_t cap){
|
||||
snprintf(out, cap, "%s", DIRTY_PATH);
|
||||
}
|
||||
#endif
|
||||
|
||||
static int path_exists(const char *p){
|
||||
struct stat st;
|
||||
return stat(p, &st) == 0;
|
||||
}
|
||||
|
||||
static int read_count(void){
|
||||
char cp[320];
|
||||
crash_path(cp, sizeof cp);
|
||||
FILE *f = fopen(cp, "rb");
|
||||
if(!f) return 0;
|
||||
int n = 0;
|
||||
if(fscanf(f, "%d", &n) != 1) n = 0;
|
||||
fclose(f);
|
||||
if(n < 0) n = 0;
|
||||
if(n > 1000) n = 1000;
|
||||
return n;
|
||||
}
|
||||
|
||||
static void write_count(int n){
|
||||
char cp[320], tmp[352];
|
||||
crash_path(cp, sizeof cp);
|
||||
snprintf(tmp, sizeof tmp, "%s.new", cp);
|
||||
FILE *f = fopen(tmp, "wb");
|
||||
if(!f) return;
|
||||
fprintf(f, "%d\n", n);
|
||||
if(fflush(f) != 0){ fclose(f); remove(tmp); return; }
|
||||
int fd = fileno(f);
|
||||
if(fd >= 0) fsync(fd);
|
||||
fclose(f);
|
||||
rename(tmp, cp);
|
||||
}
|
||||
|
||||
static void write_dirty(void){
|
||||
char dp[320], tmp[352];
|
||||
dirty_path(dp, sizeof dp);
|
||||
snprintf(tmp, sizeof tmp, "%s.new", dp);
|
||||
FILE *f = fopen(tmp, "wb");
|
||||
if(!f){
|
||||
/* best-effort: try direct write (dir may not allow tmp+rename) */
|
||||
f = fopen(dp, "wb");
|
||||
if(!f) return;
|
||||
fprintf(f, "dirty\n");
|
||||
fclose(f);
|
||||
return;
|
||||
}
|
||||
fprintf(f, "dirty\n");
|
||||
if(fflush(f) != 0){ fclose(f); remove(tmp); return; }
|
||||
int fd = fileno(f);
|
||||
if(fd >= 0) fsync(fd);
|
||||
fclose(f);
|
||||
rename(tmp, dp);
|
||||
}
|
||||
|
||||
static void clear_path(const char *p){
|
||||
remove(p);
|
||||
}
|
||||
|
||||
int health_boot_note_crash(void){
|
||||
char dp[320];
|
||||
dirty_path(dp, sizeof dp);
|
||||
int unclean = path_exists(dp);
|
||||
int n = read_count();
|
||||
/* Marker first, count second: a crash between the two still leaves
|
||||
* the marker behind, so the death is counted next boot instead of
|
||||
* being lost. */
|
||||
write_dirty();
|
||||
if(unclean){
|
||||
n += 1;
|
||||
write_count(n);
|
||||
}
|
||||
return n >= SAFE_THRESHOLD ? 1 : 0;
|
||||
}
|
||||
|
||||
void health_mark_healthy(void){
|
||||
char cp[320], dp[320];
|
||||
crash_path(cp, sizeof cp);
|
||||
dirty_path(dp, sizeof dp);
|
||||
clear_path(dp);
|
||||
clear_path(cp);
|
||||
}
|
||||
|
||||
void health_mark_clean(void){
|
||||
health_mark_healthy();
|
||||
}
|
||||
|
||||
int health_check_binary(const char *path){
|
||||
FILE *f = fopen(path, "rb");
|
||||
if(!f) return 0;
|
||||
unsigned char h[64];
|
||||
size_t n = fread(h, 1, sizeof h, f);
|
||||
long sz = -1;
|
||||
if(fseek(f, 0, SEEK_END) == 0) sz = ftell(f);
|
||||
fclose(f);
|
||||
if(n < 64 || sz <= 0) return 0;
|
||||
return updater_image_ok(h, n) && (size_t)sz <= 8u*1024u*1024u;
|
||||
}
|
||||
|
||||
int health_rollback(const char *path){
|
||||
char bak[320];
|
||||
snprintf(bak, sizeof bak, "%s.bak", path);
|
||||
FILE *f = fopen(bak, "rb");
|
||||
if(!f) return -1;
|
||||
fclose(f);
|
||||
/* Atomic replace: never remove(path) first (a failed second step
|
||||
* would leave no bootable binary at all). */
|
||||
if(rename(bak, path) != 0) return -1;
|
||||
return health_check_binary(path) ? 0 : -1;
|
||||
}
|
||||
|
||||
int health_stage_activate(const char *path){
|
||||
char tmp[320], bak[320];
|
||||
snprintf(tmp, sizeof tmp, "%s.new", path);
|
||||
snprintf(bak, sizeof bak, "%s.bak", path);
|
||||
if(!health_check_binary(tmp)){ remove(tmp); return -1; }
|
||||
/* backup current (best-effort when a prior file exists) */
|
||||
if(path_exists(path)){
|
||||
/* remove stale .bak first so rename() below cannot fail on
|
||||
* platforms without atomic replace */
|
||||
remove(bak);
|
||||
if(rename(path, bak) != 0){ remove(tmp); return -1; }
|
||||
}
|
||||
if(rename(tmp, path) != 0){
|
||||
/* activation failed: try to restore backup */
|
||||
rename(bak, path);
|
||||
remove(tmp);
|
||||
return -1;
|
||||
}
|
||||
if(!health_check_binary(path)){
|
||||
/* activated bytes somehow bad: restore backup immediately */
|
||||
remove(path);
|
||||
rename(bak, path);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int health_verify_or_rollback(const char *path){
|
||||
if(health_check_binary(path)) return 0;
|
||||
if(health_rollback(path) == 0) return 1;
|
||||
return -1;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/* health.h - crash recovery: unclean-boot marker, counter, safe mode.
|
||||
*
|
||||
* Production semantics (v1.0.1+):
|
||||
* boot: health_boot_note_crash() checks for a leftover "dirty" marker.
|
||||
* marker present -> previous boot crashed/was killed -> counter++.
|
||||
* marker absent -> previous boot was clean -> counter unchanged.
|
||||
* then writes the dirty marker for the CURRENT boot.
|
||||
* stable: health_mark_healthy() clears the marker + resets the counter.
|
||||
* call after HEALTH_STABLE_SECS of genuinely healthy runtime.
|
||||
* exit: health_mark_clean() clears both (graceful shutdown paths).
|
||||
*
|
||||
* This way three normal reboots can never trigger safe mode: each clean
|
||||
* shutdown clears the marker, so the next boot sees "clean".
|
||||
* Only consecutive UNCLEAN boots (crash before healthy) count. */
|
||||
#ifndef ORBISRPC_HEALTH_H
|
||||
#define ORBISRPC_HEALTH_H
|
||||
#define CRASH_PATH "/data/orbisRPC/crash.count"
|
||||
#define DIRTY_PATH "/data/orbisRPC/boot.dirty"
|
||||
#define SAFE_THRESHOLD 3
|
||||
/* Seconds of healthy runtime before a boot counts as clean. */
|
||||
#define HEALTH_STABLE_SECS 60
|
||||
/* Call at clean shutdown to reset the crash counter. */
|
||||
void health_mark_clean(void);
|
||||
/* Call once the daemon is stably healthy (presence flowing or uptime
|
||||
* past HEALTH_STABLE_SECS). Clears the dirty marker + resets counter. */
|
||||
void health_mark_healthy(void);
|
||||
/* Call at boot. Returns 1 when safe mode must engage (ORX-BOOT-003),
|
||||
* 0 for normal boot. Increments the persisted crash counter ONLY when
|
||||
* the previous boot left a dirty marker (i.e. crashed before healthy). */
|
||||
int health_boot_note_crash(void);
|
||||
/* Verify a staged target can run: ELF magic + size sane. 1 ok, 0 bad. */
|
||||
int health_check_binary(const char *path);
|
||||
/* Restore <path> from <path>.bak. 0 ok, -1 failed/none. */
|
||||
int health_rollback(const char *path);
|
||||
/* Verify + activate a staged "<path>.new" over <path> with backup.
|
||||
* Returns 0 activated, -1 refused (missing/invalid; .new removed,
|
||||
* live <path> untouched). Host-testable rollback primitive used by
|
||||
* the updater so staging can never leave a corrupt live binary. */
|
||||
int health_stage_activate(const char *path);
|
||||
/* Post-boot watchdog: if <path> fails health_check_binary, attempt
|
||||
* health_rollback(). Returns 0 healthy, 1 rolled back OK, -1 still bad.
|
||||
* Daemon calls this at startup for each staged target so a bad update
|
||||
* that passed staging is automatically reverted before use. */
|
||||
int health_verify_or_rollback(const char *path);
|
||||
#ifdef HEALTH_TESTABLE
|
||||
/* Override the base dir for crash.count/boot.dirty (host tests). */
|
||||
void health_set_base(const char *dir);
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,326 @@
|
||||
/* jsonlite.c */
|
||||
#include "jsonlite.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <ctype.h>
|
||||
|
||||
static jl_val_t *newval(jl_type_t t) {
|
||||
jl_val_t *v = (jl_val_t*)calloc(1, sizeof(jl_val_t));
|
||||
if (v) v->type = t;
|
||||
return v;
|
||||
}
|
||||
|
||||
/* ---- parser ---- */
|
||||
static void skip_ws(jl_parse_t *p) {
|
||||
while (p->cur < p->end) { char c = *p->cur;
|
||||
if (c==' '||c=='\t'||c=='\n'||c=='\r') p->cur++; else break; }
|
||||
}
|
||||
|
||||
static jl_val_t *parse_value(jl_parse_t *p);
|
||||
static jl_val_t *parse_value_depth(jl_parse_t *p, int depth);
|
||||
|
||||
/* Stack depth cap: network JSON nests a few levels; a hostile 100k-deep
|
||||
* array would otherwise exhaust the daemon/game thread stack. Counter
|
||||
* advances twice per nesting level, so 128 allows ~64 deep. */
|
||||
#define JL_MAX_DEPTH 128
|
||||
|
||||
static jl_val_t *parse_string(jl_parse_t *p) {
|
||||
if (p->cur >= p->end || *p->cur != '"') { p->err=1; return NULL; }
|
||||
p->cur++;
|
||||
jl_val_t *v = newval(JL_STRING); if(!v){p->err=1;return NULL;}
|
||||
size_t cap=32, len=0;
|
||||
char *buf = (char*)malloc(cap);
|
||||
if(!buf){ p->err=1; jl_free(v); return NULL; }
|
||||
while (p->cur < p->end && *p->cur != '"') {
|
||||
char c = *p->cur++;
|
||||
if (c=='\\') {
|
||||
if (p->cur >= p->end){p->err=1;break;}
|
||||
char e = *p->cur++;
|
||||
switch(e){ case '"':c='"';break; case '\\':c='\\';break; case '/':c='/';break;
|
||||
case 'b':c='\b';break; case 'f':c='\f';break; case 'n':c='\n';break;
|
||||
case 'r':c='\r';break; case 't':c='\t';break;
|
||||
case 'u':{ /* decode \uXXXX to utf8 */ if (p->cur+4>p->end){p->err=1;break;}
|
||||
unsigned cp=0; for(int i=0;i<4;i++){char h=*p->cur++;
|
||||
cp<<=4; if(h>='0'&&h<='9')cp|=(h-'0'); else if(h>='a'&&h<='f')cp|=(h-'a'+10);
|
||||
else if(h>='A'&&h<='F')cp|=(h-'A'+10); else {p->err=1;break;}}
|
||||
if(p->err)break; char outb[5]; int on=0; if(cp<0x80)outb[on++]=cp;
|
||||
else if(cp<0x800){outb[on++]=(char)(0xC0|(cp>>6));outb[on++]=(char)(0x80|(cp&0x3F));}
|
||||
else {outb[on++]=(char)(0xE0|(cp>>12));outb[on++]=(char)(0x80|((cp>>6)&0x3F));outb[on++]=(char)(0x80|(cp&0x3F));}
|
||||
if(len+(size_t)on+1>cap){
|
||||
size_t ncap=(len+(size_t)on+1)*2;
|
||||
char *nb=(char*)realloc(buf,ncap);
|
||||
if(!nb){ free(buf); jl_free(v); p->err=1; return NULL; }
|
||||
buf=nb; cap=ncap;
|
||||
}
|
||||
memcpy(buf+len,outb,on);len+=on; continue;}
|
||||
default: p->err=1; break;
|
||||
}
|
||||
if(p->err)break;
|
||||
}
|
||||
if(len+2>cap){
|
||||
size_t ncap=cap*2;
|
||||
char *nb=(char*)realloc(buf,ncap);
|
||||
if(!nb){ free(buf); jl_free(v); p->err=1; return NULL; }
|
||||
buf=nb; cap=ncap;
|
||||
}
|
||||
buf[len++]=c;
|
||||
}
|
||||
if(p->err){free(buf);jl_free(v);return NULL;}
|
||||
if(p->cur>=p->end){p->err=1;free(buf);jl_free(v);return NULL;}
|
||||
p->cur++; /* closing quote */
|
||||
buf[len]=0; v->str=buf; v->strlen=len;
|
||||
return v;
|
||||
}
|
||||
|
||||
static jl_val_t *parse_array(jl_parse_t *p, int depth) {
|
||||
if(depth > JL_MAX_DEPTH){ p->err=1; return NULL; }
|
||||
p->cur++; jl_val_t *v=newval(JL_ARRAY); if(!v){p->err=1;return NULL;}
|
||||
jl_val_t *tail=NULL;
|
||||
skip_ws(p);
|
||||
if(p->cur<p->end && *p->cur==']'){p->cur++;return v;}
|
||||
while(p->cur<p->end){
|
||||
jl_val_t *e=parse_value_depth(p, depth+1); if(p->err){jl_free(v);return NULL;}
|
||||
if(!v->child)v->child=e;else{tail->next=e;}
|
||||
tail=e; v->count++;
|
||||
skip_ws(p);
|
||||
if(p->cur<p->end&&*p->cur==','){p->cur++;skip_ws(p);continue;}
|
||||
if(p->cur<p->end&&*p->cur==']'){p->cur++;return v;}
|
||||
p->err=1;jl_free(v);return NULL;
|
||||
}
|
||||
p->err=1;jl_free(v);return NULL;
|
||||
}
|
||||
|
||||
static jl_val_t *parse_object(jl_parse_t *p, int depth) {
|
||||
if(depth > JL_MAX_DEPTH){ p->err=1; return NULL; }
|
||||
p->cur++; jl_val_t *v=newval(JL_OBJECT); if(!v){p->err=1;return NULL;}
|
||||
jl_val_t *tail=NULL;
|
||||
skip_ws(p);
|
||||
if(p->cur<p->end && *p->cur=='}'){p->cur++;return v;}
|
||||
while(p->cur<p->end){
|
||||
jl_val_t *key=parse_string(p); if(p->err){jl_free(v);return NULL;}
|
||||
skip_ws(p);
|
||||
if(p->cur<p->end&&*p->cur==':'){p->cur++;skip_ws(p);}else{p->err=1;jl_free(key);jl_free(v);return NULL;}
|
||||
jl_val_t *val=parse_value_depth(p, depth+1); if(p->err){jl_free(key);jl_free(v);return NULL;}
|
||||
jl_val_t *pair=newval(JL_OBJECT);
|
||||
if(!pair){ p->err=1; jl_free(key); jl_free(val); jl_free(v); return NULL; }
|
||||
pair->str=key->str; pair->strlen=key->strlen; pair->child=val;
|
||||
free(key); /* moved key->str into pair */
|
||||
if(!v->child)v->child=pair;else{tail->next=pair;}
|
||||
tail=pair; v->count++;
|
||||
skip_ws(p);
|
||||
if(p->cur<p->end&&*p->cur==','){p->cur++;skip_ws(p);continue;}
|
||||
if(p->cur<p->end&&*p->cur=='}'){p->cur++;return v;}
|
||||
p->err=1;jl_free(v);return NULL;
|
||||
}
|
||||
p->err=1;jl_free(v);return NULL;
|
||||
}
|
||||
|
||||
static jl_val_t *parse_value_depth(jl_parse_t *p, int depth) {
|
||||
if(depth > JL_MAX_DEPTH){ p->err=1; return NULL; }
|
||||
skip_ws(p);
|
||||
if(p->cur>=p->end){p->err=1;return NULL;}
|
||||
char c=*p->cur;
|
||||
if(c=='{')return parse_object(p, depth+1);
|
||||
if(c=='[')return parse_array(p, depth+1);
|
||||
if(c=='"')return parse_string(p);
|
||||
if(c=='t'){ if(p->end-p->cur>=4&&!memcmp(p->cur,"true",4)){p->cur+=4;jl_val_t*b=newval(JL_BOOL);if(!b){p->err=1;return NULL;}b->num=1;return b;} p->err=1;return NULL;}
|
||||
if(c=='f'){ if(p->end-p->cur>=5&&!memcmp(p->cur,"false",5)){p->cur+=5;jl_val_t*b=newval(JL_BOOL);if(!b){p->err=1;return NULL;}b->num=0;return b;} p->err=1;return NULL;}
|
||||
if(c=='n'){ if(p->end-p->cur>=4&&!memcmp(p->cur,"null",4)){p->cur+=4;jl_val_t*nv=newval(JL_NULL);if(!nv){p->err=1;return NULL;}return nv;} p->err=1;return NULL;}
|
||||
if(c=='-'||(c>='0'&&c<='9')){
|
||||
/* bounded scan: strtod needs NUL-termination but our buffer end is
|
||||
* not terminated, so copy the number token first (64 chars is far
|
||||
* beyond any real JSON number; longer fails cleanly). */
|
||||
char nb[64]; size_t ni=0;
|
||||
const char *q=p->cur;
|
||||
while(q<p->end && ni<sizeof nb-1){
|
||||
char d=*q;
|
||||
if((d>='0'&&d<='9')||d=='-'||d=='+'||d=='.'||d=='e'||d=='E'){ nb[ni++]=d; q++; }
|
||||
else break;
|
||||
}
|
||||
if(ni==0 || ni>=sizeof nb-1){ p->err=1; return NULL; }
|
||||
nb[ni]=0;
|
||||
char *ep; double dd=strtod(nb,&ep);
|
||||
if(ep==nb){ p->err=1; return NULL; }
|
||||
p->cur=q;
|
||||
jl_val_t *n=newval(JL_NUMBER); if(!n){p->err=1;return NULL;} n->num=dd; return n;
|
||||
}
|
||||
p->err=1; return NULL;
|
||||
}
|
||||
|
||||
static jl_val_t *parse_value(jl_parse_t *p){ return parse_value_depth(p, 0); }
|
||||
|
||||
jl_val_t *jl_parse(const char *s, size_t len){
|
||||
if(!s) return NULL;
|
||||
/* Input cap: no legitimate input here exceeds it (gateway frames,
|
||||
* manifests, configs are KBs); unbounded input means unbounded
|
||||
* allocation inside parse_string's doubling buffer. */
|
||||
if(len > 4u*1024u*1024u) return NULL;
|
||||
size_t l = len;
|
||||
jl_parse_t p={s,s+l,0};
|
||||
jl_val_t *v=parse_value(&p);
|
||||
skip_ws(&p);
|
||||
if(p.err || p.cur != p.end){jl_free(v);return NULL;}
|
||||
return v;
|
||||
}
|
||||
|
||||
/* Nodes: an OBJECT node is either a pair (str=key, child=value) when it is a
|
||||
* child of an object, or an object value (str=NULL, child=pair list) when it is
|
||||
* a child of an array or a pair's value. Distinguish via str to free everything. */
|
||||
void jl_free(jl_val_t *v){
|
||||
if(!v)return;
|
||||
if(v->type==JL_STRING){ if(v->str)free(v->str); free(v); return; }
|
||||
jl_val_t *c=v->child;
|
||||
while(c){
|
||||
jl_val_t *n=c->next;
|
||||
if(c->type==JL_OBJECT){
|
||||
if(c->str){ /* pair: child=value owns, str=key */
|
||||
if(c->child)jl_free(c->child);
|
||||
free(c->str);
|
||||
free(c);
|
||||
} else { /* object value: owns its whole pair list */
|
||||
jl_free(c);
|
||||
}
|
||||
} else {
|
||||
if(c->str)free(c->str);
|
||||
if(c->child)jl_free(c->child);
|
||||
free(c);
|
||||
}
|
||||
c=n;
|
||||
}
|
||||
free(v);
|
||||
}
|
||||
|
||||
jl_val_t *jl_obj_get(const jl_val_t *obj, const char *key){
|
||||
if(!obj||obj->type!=JL_OBJECT||!key)return NULL;
|
||||
jl_val_t *p=obj->child;
|
||||
while(p){
|
||||
if(p->str && !strcmp(p->str,key)){
|
||||
/* pair node -> return its value (child), unless value is missing */
|
||||
return p->child? p->child : p;
|
||||
}
|
||||
p=p->next;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
jl_val_t *jl_arr_at(const jl_val_t *arr, size_t i){
|
||||
if(!arr||arr->type!=JL_ARRAY)return NULL;
|
||||
jl_val_t *e=arr->child;
|
||||
while(e&&i--){ e=e->next; }
|
||||
return e;
|
||||
}
|
||||
|
||||
/* ---- serializer ---- */
|
||||
static int ensure_capacity(char **out, size_t *cap, size_t need);
|
||||
|
||||
static int escstr(const char *s, char **out, size_t *cap, size_t *len){
|
||||
if(!s) s = "";
|
||||
size_t l=strlen(s);
|
||||
if(l > ((size_t)-1 - *len - 3) / 6) return -1;
|
||||
size_t need=*len+l*6+2;
|
||||
if(ensure_capacity(out,cap,need+1)<0) return -1;
|
||||
char *p=*out+*len; *p++='"';
|
||||
for(size_t i=0;i<l;i++){
|
||||
char c=s[i];
|
||||
if(c=='"'||c=='\\'||(c>=0&&c<0x20)){
|
||||
*p++='\\';
|
||||
switch(c){case '"':*p++='"';break;case '\\':*p++='\\';break;
|
||||
case '\n':*p++='n';break;case '\r':*p++='r';break;case '\t':*p++='t';break;
|
||||
case '\b':*p++='b';break;case '\f':*p++='f';break;
|
||||
default:{*p++='u';*p++='0';*p++='0';const char hx[]="0123456789abcdef";
|
||||
*p++=hx[(c>>4)&0xf];*p++=hx[c&0xf];break;}}
|
||||
} else { *p++=c; }
|
||||
}
|
||||
*p++='"'; *len=(size_t)(p-*out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int ensure_capacity(char **out, size_t *cap, size_t need){
|
||||
if (need <= *cap) return 0;
|
||||
size_t ncap = *cap;
|
||||
while (ncap < need) {
|
||||
if (ncap > (size_t)-1 / 2) return -1;
|
||||
ncap *= 2;
|
||||
}
|
||||
char *p = (char *)realloc(*out, ncap);
|
||||
if (!p) return -1;
|
||||
*out = p;
|
||||
*cap = ncap;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int emit(jl_val_t *v, char **out, size_t *cap, size_t *len){
|
||||
char buf[64];
|
||||
switch(v->type){
|
||||
case JL_NULL: { size_t l=4; if(ensure_capacity(out,cap,*len+l+1)<0)return -1; memcpy(*out+*len,"null",4);*len+=4; return 0; }
|
||||
case JL_BOOL: { const char*s=v->num?"true":"false"; size_t l=strlen(s);
|
||||
if(ensure_capacity(out,cap,*len+l+1)<0)return -1; memcpy(*out+*len,s,l);*len+=l; return 0; }
|
||||
case JL_NUMBER: if(v->num_is_int) snprintf(buf,sizeof buf,"%lld",(long long)v->inum); else snprintf(buf,sizeof buf,"%.17g",v->num); break;
|
||||
case JL_STRING: return escstr(v->str,out,cap,len);
|
||||
case JL_ARRAY:{
|
||||
size_t l=*len+1; if(ensure_capacity(out,cap,l+1)<0)return -1; (*out)[(*len)++]='[';
|
||||
jl_val_t*e=v->child;int first=1;
|
||||
while(e){ if(!first){ size_t l2=*len+1; if(ensure_capacity(out,cap,l2+1)<0)return -1; (*out)[(*len)++]=','; }
|
||||
first=0; if(emit(e,out,cap,len)<0)return -1; e=e->next; }
|
||||
l=*len+1; if(ensure_capacity(out,cap,l+1)<0)return -1; (*out)[(*len)++]=']'; return 0; }
|
||||
case JL_OBJECT:{
|
||||
size_t l=*len+1; if(ensure_capacity(out,cap,l+1)<0)return -1; (*out)[(*len)++]='{';
|
||||
jl_val_t*p=v->child;int first=1;
|
||||
while(p){ if(p->str){
|
||||
if(!first){ size_t l2=*len+1; if(ensure_capacity(out,cap,l2+1)<0)return -1; (*out)[(*len)++]=','; }
|
||||
first=0;
|
||||
if(escstr(p->str,out,cap,len)<0)return -1;
|
||||
size_t l3=*len+1; if(ensure_capacity(out,cap,l3+1)<0)return -1; (*out)[(*len)++]= ':';
|
||||
if(p->child){ if(emit(p->child,out,cap,len)<0)return -1; }
|
||||
else { size_t lz=*len+4; if(ensure_capacity(out,cap,lz+1)<0)return -1; memcpy(*out+*len,"null",4);*len+=4; }
|
||||
} p=p->next; }
|
||||
l=*len+1; if(ensure_capacity(out,cap,l+1)<0)return -1; (*out)[(*len)++]='}'; return 0; }
|
||||
}
|
||||
if(buf[0]){ size_t lb=strlen(buf);
|
||||
if(ensure_capacity(out,cap,*len+lb+1)<0)return -1;
|
||||
memcpy(*out+*len,buf,lb);*len+=lb;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
char *jl_stringify(const jl_val_t *v){
|
||||
if(!v) return NULL;
|
||||
char *out=(char*)malloc(256); if(!out) return NULL;
|
||||
size_t cap=256,len=0;
|
||||
if(emit((jl_val_t*)v,&out,&cap,&len)<0){ free(out); return NULL; }
|
||||
char *t=(char*)realloc(out,len+1);
|
||||
if(t){ t[len]=0; return t; }
|
||||
out[len]=0; return out; /* realloc failed: still return what we have */
|
||||
}
|
||||
|
||||
jl_val_t *jl_new_string(const char *s){
|
||||
jl_val_t *v=newval(JL_STRING);
|
||||
if(!v) return NULL;
|
||||
v->str=strdup(s?s:"");
|
||||
if(!v->str){ free(v); return NULL; }
|
||||
v->strlen=strlen(v->str);
|
||||
return v;
|
||||
}
|
||||
jl_val_t *jl_new_number(double n){ jl_val_t *v=newval(JL_NUMBER); if(v){v->num=n; v->num_is_int=0;} return v; }
|
||||
jl_val_t *jl_new_int(int64_t n){ jl_val_t *v=newval(JL_NUMBER); if(v){v->num=(double)n; v->num_is_int=1; v->inum=n;} return v; }
|
||||
jl_val_t *jl_new_bool(int b){ jl_val_t *v=newval(JL_BOOL); if(v)v->num=b?1:0; return v; }
|
||||
jl_val_t *jl_new_null(void){ return newval(JL_NULL); }
|
||||
jl_val_t *jl_new_object(void){ return newval(JL_OBJECT); }
|
||||
jl_val_t *jl_new_array(void){ return newval(JL_ARRAY); }
|
||||
void jl_obj_set(jl_val_t *obj,const char *key,jl_val_t *val){
|
||||
if(!obj || obj->type!=JL_OBJECT || !key || !val) return;
|
||||
jl_val_t *pair=newval(JL_OBJECT);
|
||||
if(!pair) return;
|
||||
pair->str=strdup(key);
|
||||
if(!pair->str){ free(pair); return; }
|
||||
pair->strlen=strlen(key); pair->child=val;
|
||||
if(!obj->child){obj->child=pair;}else{ jl_val_t*t=obj->child; while(t->next)t=t->next; t->next=pair; }
|
||||
obj->count++;
|
||||
}
|
||||
void jl_arr_push(jl_val_t *arr,jl_val_t *val){
|
||||
if(!arr || arr->type!=JL_ARRAY || !val) return;
|
||||
if(!arr->child){arr->child=val;val->next=NULL;}
|
||||
else{ jl_val_t*t=arr->child; while(t->next)t=t->next; t->next=val; }
|
||||
arr->count++;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/* jsonlite.h - tiny JSON parser/serializer (self contained). */
|
||||
#ifndef JSONLITE_H
|
||||
#define JSONLITE_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
typedef enum { JL_NULL, JL_BOOL, JL_NUMBER, JL_STRING, JL_OBJECT, JL_ARRAY } jl_type_t;
|
||||
typedef struct jl_val {
|
||||
jl_type_t type;
|
||||
char *str; /* string text OR object-entry key */
|
||||
size_t strlen;
|
||||
double num;
|
||||
int num_is_int;
|
||||
int64_t inum;
|
||||
struct jl_val *child; /* object: first pair ; array: first elem */
|
||||
struct jl_val *next; /* object pair chain OR array elem chain */
|
||||
int32_t count;
|
||||
} jl_val_t;
|
||||
typedef struct { const char *cur; const char *end; int err; } jl_parse_t;
|
||||
jl_val_t *jl_parse(const char *s, size_t len);
|
||||
void jl_free(jl_val_t *v);
|
||||
/* NOTE: the tree is mutable; getters return non-const so results can be
|
||||
* passed to jl_obj_set / jl_arr_push without const-dropping warnings. */
|
||||
jl_val_t *jl_obj_get(const jl_val_t *obj, const char *key);
|
||||
jl_val_t *jl_arr_at(const jl_val_t *arr, size_t i);
|
||||
char *jl_stringify(const jl_val_t *v);
|
||||
jl_val_t *jl_new_string(const char *s);
|
||||
jl_val_t *jl_new_number(double n);
|
||||
jl_val_t *jl_new_int(int64_t n);
|
||||
jl_val_t *jl_new_bool(int b);
|
||||
jl_val_t *jl_new_null(void);
|
||||
jl_val_t *jl_new_object(void);
|
||||
jl_val_t *jl_new_array(void);
|
||||
void jl_obj_set(jl_val_t *obj, const char *key, jl_val_t *val);
|
||||
void jl_arr_push(jl_val_t *arr, jl_val_t *val);
|
||||
#endif
|
||||
@@ -0,0 +1,90 @@
|
||||
/* lock.c - pid lockfile with stale detection. No flock on PS4 libc,
|
||||
* so use atomic create (O_CREAT|O_EXCL) + process-table liveness
|
||||
* (sysctl, no signals needed in spawned context). */
|
||||
#include "lock.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
#include <sys/types.h>
|
||||
#include <sys/sysctl.h>
|
||||
static int pid_live(int pid){
|
||||
if(pid <= 0 || pid == (int)getpid()) return 0;
|
||||
int mib[4] = { 1, 14, 8, 0 };
|
||||
size_t sz = 0;
|
||||
if(sysctl(mib, 4, NULL, &sz, NULL, 0) != 0) return 0;
|
||||
static unsigned char buf[256*1024];
|
||||
if(sz > sizeof buf) return 1; /* huge table: assume live, fail closed */
|
||||
if(sysctl(mib, 4, buf, &sz, NULL, 0) != 0) return 0;
|
||||
size_t off = 0;
|
||||
while(off + 4 <= sz){
|
||||
int recsz = *(int *)(buf + off);
|
||||
if(recsz <= 0 || off + (size_t)recsz > sz) break;
|
||||
/* A recycled PID owned by a system daemon must not block us:
|
||||
* only a live payload process counts as a peer. Spawned
|
||||
* payloads (elfldr/GoldHEN) show up as "Payload". */
|
||||
if(recsz >= 479 && *(int *)(buf + off + 72) == pid)
|
||||
return !memcmp(buf + off + 447, "Payload", 8) &&
|
||||
buf[off + 455] == 0;
|
||||
off += (size_t)recsz;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
#include <signal.h>
|
||||
static int pid_live(int pid){
|
||||
if(pid == (int)getpid()) return 0; /* own stale lock: never "live" */
|
||||
if(kill(pid, 0) == 0) return 1;
|
||||
if(errno == EPERM) return 1;
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
static int s_held = 0;
|
||||
|
||||
int lock_acquire(void){
|
||||
int fd = open(LOCK_PATH, O_CREAT|O_EXCL|O_WRONLY, 0644);
|
||||
if(fd >= 0){
|
||||
char b[32];
|
||||
int n = snprintf(b, sizeof b, "%d\n", (int)getpid());
|
||||
if(n > 0) (void)write(fd, b, (size_t)n);
|
||||
close(fd);
|
||||
s_held = 1;
|
||||
return 0;
|
||||
}
|
||||
/* Exists: check liveness of the holder. */
|
||||
FILE *f = fopen(LOCK_PATH, "rb");
|
||||
if(!f) return -1;
|
||||
int pid = 0;
|
||||
if(fscanf(f, "%d", &pid) != 1) pid = 0;
|
||||
fclose(f);
|
||||
if(pid_live(pid)) return 1; /* live peer: stand down */
|
||||
/* Stale (holder dead or unreadable pid): reclaim. */
|
||||
remove(LOCK_PATH);
|
||||
fd = open(LOCK_PATH, O_CREAT|O_EXCL|O_WRONLY, 0644);
|
||||
if(fd < 0){
|
||||
/* Lost the race: re-check once — a live peer may hold it now. */
|
||||
f = fopen(LOCK_PATH, "rb");
|
||||
if(f){
|
||||
pid = 0;
|
||||
if(fscanf(f, "%d", &pid) != 1) pid = 0;
|
||||
fclose(f);
|
||||
if(pid_live(pid)) return 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
{
|
||||
char b[32];
|
||||
int n = snprintf(b, sizeof b, "%d\n", (int)getpid());
|
||||
if(n > 0) (void)write(fd, b, (size_t)n);
|
||||
close(fd);
|
||||
}
|
||||
s_held = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void lock_release(void){
|
||||
if(s_held){ remove(LOCK_PATH); s_held = 0; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/* lock.h - single-instance pid lockfile. */
|
||||
#ifndef ORBISRPC_LOCK_H
|
||||
#define ORBISRPC_LOCK_H
|
||||
#define LOCK_PATH "/data/orbisRPC/daemon.lock"
|
||||
/* 0 acquired, 1 live peer holds it (stand down), -1 error/retry. */
|
||||
int lock_acquire(void);
|
||||
void lock_release(void);
|
||||
#endif
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/* log.c - file logger: rotating (512 KB cap), timestamped, falls back to
|
||||
* stderr. Tries several paths because mount visibility differs between app,
|
||||
* plugin, and payload processes. Every line is mirrored to /dev/klog when
|
||||
* possible so payloads remain debuggable even with no writable mount. */
|
||||
#include "log.h"
|
||||
#include <stdarg.h>
|
||||
#include <time.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define LOG_CAP (512 * 1024)
|
||||
|
||||
static FILE *g_log = NULL;
|
||||
static int g_klog = -1;
|
||||
static volatile int g_log_lock = 0;
|
||||
static volatile int g_debug = 0;
|
||||
void log_set_debug(int on){ g_debug = on ? 1 : 0; }
|
||||
int log_is_debug(void){ return g_debug; }
|
||||
static void log_lock(void){ while(__sync_lock_test_and_set(&g_log_lock, 1)) usleep(1000); }
|
||||
static void log_unlock(void){ __sync_lock_release(&g_log_lock); }
|
||||
|
||||
/* tried in order at log_init(); first openable file wins */
|
||||
static const char *const LOG_FALLBACKS[] = {
|
||||
NULL, /* filled in with the caller's requested path */
|
||||
"/data/orbisRPC/log.txt",
|
||||
"/data/orbisrpc.log",
|
||||
"/user/temp/orbisrpc.log",
|
||||
"/mnt/sandbox/orbisrpc.log",
|
||||
"/orbisrpc.log",
|
||||
};
|
||||
|
||||
void log_init(const char *path) {
|
||||
if (g_log) return;
|
||||
if(path) {
|
||||
g_log = fopen(path, "ab");
|
||||
if(g_log){
|
||||
/* cap check AFTER open to avoid stat->remove TOCTOU */
|
||||
struct stat st;
|
||||
if(fstat(fileno(g_log), &st)==0 && st.st_size > LOG_CAP){
|
||||
fclose(g_log); g_log = NULL;
|
||||
remove(path);
|
||||
g_log = fopen(path, "ab");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!g_log) {
|
||||
for (unsigned i = 0; i < sizeof(LOG_FALLBACKS)/sizeof(LOG_FALLBACKS[0]); i++) {
|
||||
const char *p = LOG_FALLBACKS[i];
|
||||
if (!p || !path || strcmp(p, path) == 0) continue;
|
||||
g_log = fopen(p, "ab");
|
||||
if (g_log) break;
|
||||
}
|
||||
}
|
||||
if (g_log) setvbuf(g_log, NULL, _IONBF, 0);
|
||||
/* klog mirror: survives even when every mount is invisible */
|
||||
g_klog = open("/dev/klog", O_WRONLY);
|
||||
if(g_klog < 0) g_klog = -1;
|
||||
}
|
||||
|
||||
void log_msg(const char *fmt, ...) {
|
||||
if (!fmt) return;
|
||||
log_lock();
|
||||
if (!g_log) { g_log = stderr; }
|
||||
va_list ap, ap2;
|
||||
va_start(ap, fmt);
|
||||
va_copy(ap2, ap); /* copy BEFORE vfprintf consumes ap */
|
||||
time_t t = time(NULL);
|
||||
struct tm tmv; struct tm *tm = NULL;
|
||||
/* gmtime_r is our thread-safe shim; UTC is fine for logs */
|
||||
extern struct tm *gmtime_r(const time_t *, struct tm *);
|
||||
tm = gmtime_r(&t, &tmv);
|
||||
char ts[20];
|
||||
if(tm) strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", tm);
|
||||
else { strncpy(ts, "1970-01-01 00:00:00", sizeof ts); ts[sizeof ts-1]=0; }
|
||||
fprintf(g_log, "[%s] ", ts);
|
||||
vfprintf(g_log, fmt, ap);
|
||||
fprintf(g_log, "\n");
|
||||
fflush(g_log);
|
||||
va_end(ap);
|
||||
if (g_klog >= 0) {
|
||||
char line[512];
|
||||
int n = snprintf(line, sizeof line, "[orbisRPC %s] ", ts);
|
||||
if (n > 0 && n < (int)sizeof line)
|
||||
n += vsnprintf(line + n, sizeof line - n, fmt, ap2);
|
||||
if (n > 0) { if (n >= (int)sizeof line) n = (int)sizeof line - 1; line[n++] = '\n'; (void)write(g_klog, line, (size_t)n); }
|
||||
}
|
||||
va_end(ap2);
|
||||
log_unlock();
|
||||
}
|
||||
|
||||
void log_close(void) {
|
||||
if (g_log && g_log != stderr) fclose(g_log);
|
||||
g_log = NULL;
|
||||
if (g_klog >= 0) { close(g_klog); g_klog = -1; }
|
||||
}
|
||||
|
||||
void log_dbg(const char *fmt, ...) {
|
||||
if (!g_debug || !fmt) return;
|
||||
log_lock();
|
||||
if (!g_log) { g_log = stderr; }
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
time_t t = time(NULL);
|
||||
struct tm tmv; struct tm *tm = NULL;
|
||||
extern struct tm *gmtime_r(const time_t *, struct tm *);
|
||||
tm = gmtime_r(&t, &tmv);
|
||||
char ts[20];
|
||||
if(tm) strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", tm);
|
||||
else { strncpy(ts, "1970-01-01 00:00:00", sizeof ts); ts[sizeof ts-1]=0; }
|
||||
fprintf(g_log, "[%s] DBG ", ts);
|
||||
vfprintf(g_log, fmt, ap);
|
||||
fprintf(g_log, "\n");
|
||||
fflush(g_log);
|
||||
va_end(ap);
|
||||
log_unlock();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/* log.h - timestamped file logger with stderr and optional kernel-log fallback. */
|
||||
#ifndef LOG_H
|
||||
#define LOG_H
|
||||
#include <stdio.h>
|
||||
/* Selects the first writable log path, falling back to stderr if needed. */
|
||||
void log_init(const char *path);
|
||||
void log_msg(const char *fmt, ...);
|
||||
/* Debug logging: compiled in, emitted only when log_set_debug(1) is on.
|
||||
* Zero overhead otherwise. For per-poll detail — never for secrets. */
|
||||
void log_set_debug(int on);
|
||||
int log_is_debug(void);
|
||||
void log_dbg(const char *fmt, ...);
|
||||
void log_close(void);
|
||||
#endif
|
||||
@@ -0,0 +1,12 @@
|
||||
/* main.c - orbisRPC payload entry: detect game -> Discord presence.
|
||||
* Runs as a GoldHEN payload (BinLoader push) or standalone ELF. */
|
||||
#include "daemon.h"
|
||||
#include "log.h"
|
||||
#include <stddef.h>
|
||||
|
||||
int main(int argc, char **argv){
|
||||
(void)argc;(void)argv;
|
||||
log_init("/data/orbisRPC/log.txt");
|
||||
log_msg("orbisRPC payload boot");
|
||||
return daemon_run(NULL);
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/* manifest.c - signed release manifest (host-testable + PS4).
|
||||
* Parsing via jsonlite; hashing + ECDSA via mbedTLS (already linked). */
|
||||
#include "manifest.h"
|
||||
#include "jsonlite.h"
|
||||
#include "updater.h"
|
||||
#include <string.h>
|
||||
#include <mbedtls/sha256.h>
|
||||
#include <mbedtls/ecdsa.h>
|
||||
#include <mbedtls/ecp.h>
|
||||
#include <mbedtls/bignum.h>
|
||||
|
||||
int manifest_sha256_hex(const unsigned char *data, size_t n, char out[65]){
|
||||
unsigned char dig[32];
|
||||
if(!data && n) return -1;
|
||||
mbedtls_sha256_context sc;
|
||||
mbedtls_sha256_init(&sc);
|
||||
int ok = mbedtls_sha256_starts(&sc, 0) == 0 &&
|
||||
mbedtls_sha256_update(&sc, data ? data : (const unsigned char *)"", n) == 0 &&
|
||||
mbedtls_sha256_finish(&sc, dig) == 0;
|
||||
mbedtls_sha256_free(&sc);
|
||||
if(!ok) return -1;
|
||||
for(int i = 0; i < 32; i++) snprintf(out + 2 * i, 3, "%02x", dig[i]);
|
||||
out[64] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int copy_str(const jl_val_t *v, char *out, size_t cap){
|
||||
if(!v || v->type != JL_STRING || !v->str) return -1;
|
||||
size_t n = strlen(v->str);
|
||||
if(n >= cap) return -1;
|
||||
memcpy(out, v->str, n + 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int manifest_parse(const char *json, size_t len, manifest_t *out){
|
||||
if(!json || !out) return -1;
|
||||
memset(out, 0, sizeof *out);
|
||||
jl_val_t *r = jl_parse(json, len);
|
||||
if(!r || r->type != JL_OBJECT){ if(r) jl_free(r); return -1; }
|
||||
if(copy_str(jl_obj_get(r, "version"), out->version, sizeof out->version) != 0){
|
||||
jl_free(r); return -1;
|
||||
}
|
||||
/* optional with sane defaults */
|
||||
const jl_val_t *ch = jl_obj_get(r, "channel");
|
||||
if(ch && ch->type == JL_STRING){
|
||||
if(copy_str(ch, out->channel, sizeof out->channel) != 0){ jl_free(r); return -1; }
|
||||
} else {
|
||||
memcpy(out->channel, "stable", 7);
|
||||
}
|
||||
const jl_val_t *mv = jl_obj_get(r, "minimum_version");
|
||||
if(!mv) mv = jl_obj_get(r, "min_version");
|
||||
if(mv && mv->type == JL_STRING){
|
||||
if(copy_str(mv, out->min_version, sizeof out->min_version) != 0){ jl_free(r); return -1; }
|
||||
}
|
||||
const jl_val_t *pl = jl_obj_get(r, "platform");
|
||||
if(!pl) pl = jl_obj_get(r, "supported_platform");
|
||||
if(pl && pl->type == JL_STRING){
|
||||
if(copy_str(pl, out->platform, sizeof out->platform) != 0){ jl_free(r); return -1; }
|
||||
}
|
||||
const jl_val_t *assets = jl_obj_get(r, "assets");
|
||||
if(!assets) assets = jl_obj_get(r, "files");
|
||||
if(assets && assets->type == JL_OBJECT){
|
||||
/* object form: { "orbisrpc.bin": "<hex>", ... } */
|
||||
/* jsonlite has no object iterator in public API; fall back to
|
||||
* array form below. Object form unsupported -> invalid. */
|
||||
jl_free(r);
|
||||
return -1;
|
||||
}
|
||||
if(assets && assets->type == JL_ARRAY){
|
||||
int n = 0;
|
||||
for(size_t i = 0; ; i++){
|
||||
const jl_val_t *a = jl_arr_at(assets, i);
|
||||
if(!a) break;
|
||||
if(n >= MANIFEST_MAX_ASSETS) break;
|
||||
const jl_val_t *nm = jl_obj_get(a, "name");
|
||||
const jl_val_t *sh = jl_obj_get(a, "sha256");
|
||||
if(!sh) sh = jl_obj_get(a, "hash");
|
||||
if(!nm || nm->type != JL_STRING || !sh || sh->type != JL_STRING)
|
||||
continue;
|
||||
if(strlen(nm->str) >= sizeof out->assets[n].name) continue;
|
||||
if(strlen(sh->str) != 64) continue;
|
||||
int hexok = 1;
|
||||
for(int k = 0; k < 64; k++){
|
||||
char c = sh->str[k];
|
||||
if(!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') ||
|
||||
(c >= 'A' && c <= 'F'))){ hexok = 0; break; }
|
||||
}
|
||||
if(!hexok) continue;
|
||||
memcpy(out->assets[n].name, nm->str, strlen(nm->str) + 1);
|
||||
for(int k = 0; k < 64; k++){
|
||||
char c = sh->str[k];
|
||||
out->assets[n].sha256[k] = (c >= 'A' && c <= 'F') ? (char)(c - 'A' + 'a') : c;
|
||||
}
|
||||
out->assets[n].sha256[64] = 0;
|
||||
n++;
|
||||
}
|
||||
out->nassets = n;
|
||||
}
|
||||
jl_free(r);
|
||||
if(!out->version[0] || out->nassets <= 0) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int manifest_find(const manifest_t *m, const char *name, char out_hex[65]){
|
||||
if(!m || !name) return -1;
|
||||
for(int i = 0; i < m->nassets; i++){
|
||||
if(!strcmp(m->assets[i].name, name)){
|
||||
if(out_hex) memcpy(out_hex, m->assets[i].sha256, 65);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int manifest_check(const manifest_t *m, const char *name,
|
||||
const unsigned char *data, size_t n){
|
||||
char want[65], got[65];
|
||||
if(manifest_find(m, name, want) != 0) return -1;
|
||||
if(manifest_sha256_hex(data, n, got) != 0) return -1;
|
||||
if(strcmp(got, want) != 0) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int manifest_is_newer(const manifest_t *m, const char *local_version){
|
||||
if(!m || !local_version) return 0;
|
||||
return updater_cmp(m->version, local_version) > 0;
|
||||
}
|
||||
|
||||
int manifest_gate(const manifest_t *m){
|
||||
if(!m) return 0;
|
||||
if(m->channel[0] && strcmp(m->channel, "stable") != 0) return 0;
|
||||
if(m->platform[0] && strcmp(m->platform, "ps4-goldhen") != 0 &&
|
||||
strcmp(m->platform, "ps4") != 0)
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int manifest_verify_sig(const unsigned char *msg, size_t msglen,
|
||||
const unsigned char sig[64],
|
||||
const unsigned char pubkey[64]){
|
||||
if(!msg || !sig || !pubkey) return -1;
|
||||
unsigned char hash[32];
|
||||
{
|
||||
mbedtls_sha256_context sc;
|
||||
mbedtls_sha256_init(&sc);
|
||||
int ok = mbedtls_sha256_starts(&sc, 0) == 0 &&
|
||||
mbedtls_sha256_update(&sc, msg, msglen) == 0 &&
|
||||
mbedtls_sha256_finish(&sc, hash) == 0;
|
||||
mbedtls_sha256_free(&sc);
|
||||
if(!ok) return -1;
|
||||
}
|
||||
mbedtls_ecp_group grp;
|
||||
mbedtls_ecp_point Q;
|
||||
mbedtls_mpi r, s;
|
||||
mbedtls_ecp_group_init(&grp);
|
||||
mbedtls_ecp_point_init(&Q);
|
||||
mbedtls_mpi_init(&r); mbedtls_mpi_init(&s);
|
||||
int rc = -1;
|
||||
unsigned char uncompressed[65];
|
||||
if(mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1) != 0) goto out;
|
||||
if(mbedtls_mpi_read_binary(&r, sig, 32) != 0) goto out;
|
||||
if(mbedtls_mpi_read_binary(&s, sig + 32, 32) != 0) goto out;
|
||||
/* Public API only (no struct internals): uncompressed point 0x04||X||Y. */
|
||||
uncompressed[0] = 0x04;
|
||||
memcpy(uncompressed + 1, pubkey, 64);
|
||||
if(mbedtls_ecp_point_read_binary(&grp, &Q, uncompressed,
|
||||
sizeof uncompressed) != 0) goto out;
|
||||
if(mbedtls_ecp_check_pubkey(&grp, &Q) != 0) goto out;
|
||||
if(mbedtls_ecdsa_verify(&grp, hash, sizeof hash, &Q, &r, &s) != 0) goto out;
|
||||
rc = 0;
|
||||
out:
|
||||
mbedtls_ecp_group_free(&grp);
|
||||
mbedtls_ecp_point_free(&Q);
|
||||
mbedtls_mpi_free(&r); mbedtls_mpi_free(&s);
|
||||
return rc;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/* manifest.h - signed release manifest: parse, policy, signature verify.
|
||||
*
|
||||
* Release channel trust (v1.0.1+):
|
||||
* manifest.json (version, channel, min_version, platform, asset sha256s)
|
||||
* manifest.sig (raw 64-byte ECDSA P-256 r||s over manifest.json bytes)
|
||||
* verified with the embedded release public key (release_pubkey.h).
|
||||
* Policy: manifest+valid sig REQUIRED for update; SHA256SUMS is a
|
||||
* compat fallback only when no manifest exists; ELF-only is refused.
|
||||
*/
|
||||
#ifndef ORBISRPC_MANIFEST_H
|
||||
#define ORBISRPC_MANIFEST_H
|
||||
#include <stddef.h>
|
||||
|
||||
#define MANIFEST_MAX_ASSETS 8
|
||||
|
||||
typedef struct {
|
||||
char name[64];
|
||||
char sha256[65];
|
||||
} manifest_asset_t;
|
||||
|
||||
typedef struct {
|
||||
char version[32];
|
||||
char channel[16];
|
||||
char min_version[32];
|
||||
char platform[32];
|
||||
manifest_asset_t assets[MANIFEST_MAX_ASSETS];
|
||||
int nassets;
|
||||
} manifest_t;
|
||||
|
||||
/* Parse manifest.json bytes. 0 ok, -1 invalid. */
|
||||
int manifest_parse(const char *json, size_t len, manifest_t *out);
|
||||
/* Find asset hash by filename. 0 ok + out_hex, -1 not listed. */
|
||||
int manifest_find(const manifest_t *m, const char *name, char out_hex[65]);
|
||||
/* Check data against the listed hash for name. 0 match, -1 mismatch/unlisted. */
|
||||
int manifest_check(const manifest_t *m, const char *name,
|
||||
const unsigned char *data, size_t n);
|
||||
/* 1 when manifest version is newer than local, 0 otherwise. */
|
||||
int manifest_is_newer(const manifest_t *m, const char *local_version);
|
||||
/* Channel/platform gate: 1 accepted (stable/ps4-goldhen), 0 refused. */
|
||||
int manifest_gate(const manifest_t *m);
|
||||
/* Verify raw ECDSA P-256 signature (r||s, 64 bytes) over msg with raw
|
||||
* pubkey (X||Y, 64 bytes). 0 valid, -1 invalid/error. */
|
||||
int manifest_verify_sig(const unsigned char *msg, size_t msglen,
|
||||
const unsigned char sig[64],
|
||||
const unsigned char pubkey[64]);
|
||||
/* SHA256 of buffer as lowercase hex (64+NUL). 0 ok. */
|
||||
int manifest_sha256_hex(const unsigned char *data, size_t n, char out[65]);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,22 @@
|
||||
/* release_pubkey.h - embedded release-channel public key (P-256, raw X||Y).
|
||||
*
|
||||
* DEV KEY: generated for v1.0.1 development. Before publishing a real
|
||||
* release, generate a production keypair OFFLINE, replace these bytes,
|
||||
* and keep the private key out of the repo:
|
||||
* openssl ecparam -name prime256v1 -genkey -noout -out release_priv.pem
|
||||
* openssl ec -in release_priv.pem -pubout -out release_pub.pem
|
||||
* Sign manifest.json with scripts/make_manifest.py (or openssl dgst).
|
||||
*/
|
||||
#ifndef ORBISRPC_RELEASE_PUBKEY_H
|
||||
#define ORBISRPC_RELEASE_PUBKEY_H
|
||||
|
||||
/* Uncompressed P-256 X || Y (64 bytes). */
|
||||
static const unsigned char ORBISRPC_RELEASE_PUBKEY[64] = {
|
||||
0x47,0xb8,0x9d,0xb8,0x85,0x3d,0x6c,0xcf,0x7e,0x1a,0xbe,0x6e,0xd1,0x5c,0x2e,0x69,
|
||||
0x66,0x2f,0xa8,0x09,0xbf,0x6d,0xf8,0x9c,0x50,0xe4,0x10,0x0b,0x79,0x4b,0x66,0x1a,
|
||||
0xd7,0x79,0xae,0x2e,0x41,0xc9,0x35,0x5c,0x6f,0xe3,0xba,0xed,0xc9,0x93,0x9a,0xca,
|
||||
0xe8,0x18,0xd5,0x4b,0xeb,0x84,0x20,0x8d,0xb3,0xd2,0x4a,0xfc,0x65,0x81,0x6c,0x6f
|
||||
};
|
||||
#define ORBISRPC_RELEASE_KEY_ID "dev-2026-09-20"
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,67 @@
|
||||
/* sfo.c - PARAM.SFO TITLE extractor (bounds-checked, no libc beyond string).
|
||||
* Every PS4 game carries sce_sys/param.sfo with a TITLE field. Inside the
|
||||
* game process it is reachable via the app0 mount; the plugin tries those
|
||||
* paths first because they work on ANY console with zero setup. */
|
||||
#include "sfo.h"
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#pragma pack(push, 1)
|
||||
typedef struct {
|
||||
uint32_t magic; /* 0x46535000 ("\0PSF" little-endian) */
|
||||
uint32_t version; /* usually 0x0201 */
|
||||
uint32_t key_off;
|
||||
uint32_t data_off;
|
||||
uint32_t count;
|
||||
} sfo_hdr_t;
|
||||
typedef struct {
|
||||
uint16_t key_off;
|
||||
uint16_t fmt; /* 0x0004 = utf8 string (null-padded), 0x0404 = uint32 */
|
||||
uint32_t len; /* used bytes (incl. null for strings) */
|
||||
uint32_t max_len;
|
||||
uint32_t data_off; /* relative to data_off base */
|
||||
} sfo_entry_t;
|
||||
#pragma pack(pop)
|
||||
|
||||
int sfo_title(const unsigned char *buf, size_t n, char *out, size_t cap){
|
||||
if(!buf || !out || cap == 0) return -1;
|
||||
out[0] = 0;
|
||||
if(n < sizeof(sfo_hdr_t)) return -1;
|
||||
const sfo_hdr_t *h = (const sfo_hdr_t *)buf;
|
||||
if(h->magic != 0x46535000u) return -1;
|
||||
if(h->count == 0 || h->count > 256) return -1;
|
||||
if(h->key_off >= n || h->data_off >= n) return -1;
|
||||
if(sizeof(sfo_hdr_t) + (size_t)h->count * sizeof(sfo_entry_t) > n) return -1;
|
||||
const sfo_entry_t *e = (const sfo_entry_t *)(buf + sizeof(sfo_hdr_t));
|
||||
for(uint32_t i = 0; i < h->count; i++){
|
||||
size_t ko = (size_t)h->key_off + e[i].key_off;
|
||||
if(ko >= n) continue;
|
||||
/* keys must be null-terminated INSIDE the buffer: bound the scan */
|
||||
size_t kmax = n - ko;
|
||||
size_t klen = 0;
|
||||
while(klen < kmax && buf[ko + klen]) klen++;
|
||||
if(klen >= kmax) continue; /* unterminated key: malformed */
|
||||
const char *key = (const char *)(buf + ko);
|
||||
/* keys of interest: TITLE first, then language variants TITLE_XX */
|
||||
int is_title = (!strcmp(key, "TITLE") ||
|
||||
(klen == 8 && !memcmp(key, "TITLE_", 6)));
|
||||
if(!is_title) continue;
|
||||
if(e[i].fmt != 0x0004 && e[i].fmt != 0x0204) continue;
|
||||
size_t dlen = e[i].len;
|
||||
if(dlen == 0 || dlen > 256) continue;
|
||||
size_t doff = (size_t)h->data_off + e[i].data_off;
|
||||
if(doff + dlen > n) continue;
|
||||
/* copy up to null, require printable ASCII/UTF-8 start */
|
||||
size_t ci = 0;
|
||||
for(size_t k = 0; k < dlen && ci < cap - 1; k++){
|
||||
unsigned char c = buf[doff + k];
|
||||
if(c == 0) break;
|
||||
if(c < 0x20 && c != 0x20) break;
|
||||
out[ci++] = (char)c;
|
||||
}
|
||||
out[ci] = 0;
|
||||
if(ci > 1) return 0;
|
||||
out[0] = 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/* sfo.h - PARAM.SFO TITLE extractor. */
|
||||
#ifndef SFO_H
|
||||
#define SFO_H
|
||||
#include <stddef.h>
|
||||
/* Returns 0 and writes the TITLE on success, -1 otherwise. Never reads
|
||||
* past buf+n. */
|
||||
int sfo_title(const unsigned char *buf, size_t n, char *out, size_t cap);
|
||||
#endif
|
||||
@@ -0,0 +1,116 @@
|
||||
/* timesync.c - minimal SNTP client (RFC 5905, client mode only).
|
||||
* One UDP exchange per call; 3s I/O budget; any failure keeps the old
|
||||
* offset. Host-testable (POSIX only). */
|
||||
#include "timesync.h"
|
||||
#include "log.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <stdint.h>
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#else
|
||||
#include <orbis/Net.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/socket.h>
|
||||
#endif
|
||||
|
||||
#define NTP_EPOCH_OFFSET 2208988800u
|
||||
#define NTP_PORT 123
|
||||
|
||||
static int64_t s_offset = 0;
|
||||
static int s_synced = 0;
|
||||
|
||||
static int sntp_once(const char *host){
|
||||
unsigned char pkt[48];
|
||||
memset(pkt, 0, sizeof pkt);
|
||||
pkt[0] = 0x1b; /* LI=0, VN=3, Mode=3 (client) */
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
struct addrinfo hints, *res = NULL;
|
||||
memset(&hints, 0, sizeof hints);
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_DGRAM;
|
||||
{
|
||||
char portbuf[8];
|
||||
snprintf(portbuf, sizeof portbuf, "%d", NTP_PORT);
|
||||
if(getaddrinfo(host, portbuf, &hints, &res) != 0 || !res) return -1;
|
||||
}
|
||||
int fd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if(fd < 0){ freeaddrinfo(res); return -1; }
|
||||
struct timeval tv = { 3, 0 };
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||
int ok = -1;
|
||||
/* Connect the UDP socket: recv() then only accepts the peer we
|
||||
* queried, instead of any spoofed datagram on the LAN. */
|
||||
if(connect(fd, res->ai_addr, res->ai_addrlen) != 0){
|
||||
freeaddrinfo(res);
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
if(send(fd, pkt, sizeof pkt, 0) == (int)sizeof pkt){
|
||||
unsigned char rep[48];
|
||||
ssize_t n = recv(fd, (char *)rep, sizeof rep, 0);
|
||||
if(n >= 48){
|
||||
uint32_t tx;
|
||||
memcpy(&tx, rep + 40, 4);
|
||||
tx = ((tx & 0xff) << 24) | ((tx & 0xff00) << 8) |
|
||||
((tx & 0xff0000) >> 8) | ((tx & 0xff000000) >> 24);
|
||||
/* Sanity window Nov 2023..Dec 2034 (uint32 NTP wraps 2036):
|
||||
* a broken/malicious server cannot fling our clock. */
|
||||
if(tx > NTP_EPOCH_OFFSET + 1700000000u &&
|
||||
tx < NTP_EPOCH_OFFSET + 2050000000u){
|
||||
int64_t ntp_unix = (int64_t)(tx - NTP_EPOCH_OFFSET);
|
||||
s_offset = ntp_unix - (int64_t)time(NULL);
|
||||
s_synced = 1;
|
||||
ok = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
freeaddrinfo(res);
|
||||
close(fd);
|
||||
return ok;
|
||||
#else
|
||||
(void)host;
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
int time_sync(void){
|
||||
static const char *hosts[] = {
|
||||
"time.google.com", "pool.ntp.org", "time.cloudflare.com", NULL
|
||||
};
|
||||
/* One host per call, rotating: a full 3-host sweep can stall ~20s+,
|
||||
* which must never sit inside the 1s gateway loop (heartbeat
|
||||
* starvation). Boot sweeps via time_sync_all(); hourly ticks call
|
||||
* this (single ~6s-bounded attempt). */
|
||||
static int next_host = 0;
|
||||
const char *h = hosts[next_host];
|
||||
next_host = (next_host + 1) % 3;
|
||||
if(sntp_once(h) == 0){
|
||||
log_msg("time sync ok via %s (offset %+llds)",
|
||||
h, (long long)s_offset);
|
||||
return 0;
|
||||
}
|
||||
log_msg("time sync via %s failed; using local clock (offset %+llds)",
|
||||
h, (long long)s_offset);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Boot sweep: try all hosts (bounded, once per boot, before first
|
||||
* connect — never in the live loop). */
|
||||
int time_sync_all(void){
|
||||
for(int i = 0; i < 3; i++){
|
||||
if(time_sync() == 0) return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int64_t time_fixed(void){
|
||||
return (int64_t)time(NULL) + s_offset;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/* timesync.h - SNTP wall-clock correction for Discord timestamps.
|
||||
*
|
||||
* Jailbroken consoles commonly block Sony servers (ban avoidance), which
|
||||
* also kills PSN time sync — the PS4 clock then drifts (observed: +38min
|
||||
* and growing, plus wrong timezones). Discord timestamps must be true
|
||||
* UTC or the timer wedges at 0:00, so the daemon syncs itself against
|
||||
* public NTP (no Sony involved) and stamps sessions with corrected time.
|
||||
* Failure degrades to the local clock (old behavior), never fatal. */
|
||||
#ifndef ORBISRPC_TIMESYNC_H
|
||||
#define ORBISRPC_TIMESYNC_H
|
||||
#include <stdint.h>
|
||||
/* Try an SNTP exchange; on success stores offset and returns 0. -1 = keep
|
||||
* old offset (first boot without sync behaves like before). */
|
||||
int time_sync(void);
|
||||
/* Boot sweep: try all hosts (bounded, once per boot, before first
|
||||
* connect — never in the live loop). */
|
||||
int time_sync_all(void);
|
||||
/* Corrected wall clock for Discord epoch stamps. */
|
||||
int64_t time_fixed(void);
|
||||
#endif
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
/* tls.c - mbedTLS-backed TLS client for orbisRPC.
|
||||
*
|
||||
* Why mbedTLS and not BearSSL: Cloudflare (fronting gateway.discord.gg)
|
||||
* silently drops handshakes whose ClientHello looks non-browser. BearSSL
|
||||
* cannot offer session tickets, EMS or encrypt-then-MAC at all, so its
|
||||
* fingerprint always scores as a bot and the server ghosts us after the
|
||||
* handshake (established TLS, zero HTTP bytes back — verified on-host).
|
||||
* mbedTLS sends the standard extension set and gets 101 immediately.
|
||||
*
|
||||
* The socket stays non-blocking; WANT_READ/WRITE maps to our pump model.
|
||||
* Certificate verification is REQUIRED against the curated bundle
|
||||
* (ca_bundle_pem.h); TLS is capped at 1.2 because mbedTLS 3.x routes 1.3
|
||||
* through PSA crypto whose init fails on-console. SNI is sent. */
|
||||
#include "tls.h"
|
||||
#include "log.h"
|
||||
#include "clock.h"
|
||||
#include <mbedtls/ssl.h>
|
||||
#include <mbedtls/entropy.h>
|
||||
#include <mbedtls/ctr_drbg.h>
|
||||
#include <mbedtls/debug.h>
|
||||
#include <mbedtls/net_sockets.h> /* error codes only; transport is ours */
|
||||
#include <mbedtls/x509_crt.h>
|
||||
#include "ca_bundle_pem.h"
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
#include <sys/socket.h>
|
||||
#include <errno.h>
|
||||
#else
|
||||
#include <orbis/Net.h>
|
||||
#endif
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
|
||||
/* mbedTLS internal trace -> our log: turns "handshake fail" from a bare
|
||||
* code into the exact failing step. */
|
||||
static void tls_mbedtls_dbg(void *ctx, int level,
|
||||
const char *file, int line, const char *str){
|
||||
(void)ctx;
|
||||
/* mbedTLS chat (levels 1-3) only in debug mode: at threshold 3 every
|
||||
* TLS record logs a line, which is pure I/O load in production. */
|
||||
if(level > 0 && !log_is_debug()) return;
|
||||
const char *base = strrchr(file, '/');
|
||||
base = base ? base + 1 : file;
|
||||
size_t n = strlen(str);
|
||||
while(n > 0 && (str[n-1] == '\n' || str[n-1] == '\r')) n--;
|
||||
log_msg("mbedTLS %s:%d: %.*s", base, line, (int)n, str);
|
||||
}
|
||||
|
||||
struct tls_ctx {
|
||||
mbedtls_ssl_context ssl;
|
||||
mbedtls_ssl_config conf;
|
||||
mbedtls_ctr_drbg_context rng;
|
||||
mbedtls_entropy_context ent;
|
||||
mbedtls_x509_crt ca;
|
||||
int fd;
|
||||
};
|
||||
|
||||
/* Strong entropy from the OS ONLY (/dev/urandom). There is deliberately
|
||||
* no weak fallback: inventing cryptographic randomness from time/address
|
||||
* jitter would silently downgrade every TLS session and the DRBG seed.
|
||||
* If strong entropy is unavailable the handshake fails closed here. */
|
||||
static int orbis_poll(void *data, unsigned char *out, size_t len, size_t *olen){
|
||||
(void)data;
|
||||
int fd = open("/dev/urandom", O_RDONLY);
|
||||
if(fd < 0) return -1;
|
||||
size_t got = 0;
|
||||
while(got < len){
|
||||
long r = read(fd, (char *)out + got, len - got);
|
||||
if(r <= 0){ close(fd); return -1; }
|
||||
got += (size_t)r;
|
||||
}
|
||||
close(fd);
|
||||
*olen = len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int net_send(void *ctx, const unsigned char *b, size_t n){
|
||||
tls_ctx_t *t = (tls_ctx_t *)ctx;
|
||||
size_t cap = n > 32768 ? 32768 : n;
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
int r = (int)send(t->fd, b, cap, 0);
|
||||
if(r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
||||
return MBEDTLS_ERR_SSL_WANT_WRITE;
|
||||
if(r < 0) return MBEDTLS_ERR_NET_SEND_FAILED;
|
||||
return r;
|
||||
#else
|
||||
int r = (int)sceNetSend(t->fd, b, (int)cap, 0);
|
||||
if(r < 0) return MBEDTLS_ERR_SSL_WANT_WRITE; /* NBIO: retry till deadline */
|
||||
return r;
|
||||
#endif
|
||||
}
|
||||
|
||||
static int net_recv(void *ctx, unsigned char *b, size_t n){
|
||||
tls_ctx_t *t = (tls_ctx_t *)ctx;
|
||||
size_t cap = n > 16384 ? 16384 : n;
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
int r = (int)recv(t->fd, b, cap, 0);
|
||||
if(r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
||||
return MBEDTLS_ERR_SSL_WANT_READ;
|
||||
if(r < 0) return MBEDTLS_ERR_NET_RECV_FAILED;
|
||||
return r;
|
||||
#else
|
||||
int r = (int)sceNetRecv(t->fd, b, (int)cap, 0);
|
||||
if(r < 0) return MBEDTLS_ERR_SSL_WANT_READ; /* NBIO: retry till deadline */
|
||||
return r;
|
||||
#endif
|
||||
}
|
||||
|
||||
tls_ctx_t *tls_start(int fd, const char *host){
|
||||
tls_ctx_t *t = (tls_ctx_t *)calloc(1, sizeof *t);
|
||||
if(!t) return NULL;
|
||||
t->fd = fd;
|
||||
int rc = 0;
|
||||
|
||||
mbedtls_ssl_init(&t->ssl);
|
||||
mbedtls_ssl_config_init(&t->conf);
|
||||
mbedtls_x509_crt_init(&t->ca);
|
||||
mbedtls_ctr_drbg_init(&t->rng);
|
||||
mbedtls_entropy_init(&t->ent);
|
||||
mbedtls_entropy_add_source(&t->ent, orbis_poll, NULL, 64,
|
||||
MBEDTLS_ENTROPY_SOURCE_STRONG);
|
||||
|
||||
if((rc = mbedtls_ctr_drbg_seed(&t->rng, mbedtls_entropy_func, &t->ent,
|
||||
(const unsigned char *)"orbisRPC", 8)) != 0){
|
||||
log_msg("tls: rng seed fail %d", rc);
|
||||
goto fail;
|
||||
}
|
||||
if((rc = mbedtls_ssl_config_defaults(&t->conf, MBEDTLS_SSL_IS_CLIENT,
|
||||
MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT)) != 0){
|
||||
log_msg("tls: config fail %d", rc);
|
||||
goto fail;
|
||||
}
|
||||
/* Curated trust anchors: chain verification is REQUIRED. A handshake
|
||||
* that does not verify against the bundle fails closed (no MITM). */
|
||||
if((rc = mbedtls_x509_crt_parse(&t->ca,
|
||||
(const unsigned char *)ORBISRPC_CA_BUNDLE_PEM,
|
||||
sizeof ORBISRPC_CA_BUNDLE_PEM)) < 0){
|
||||
log_msg("tls: CA bundle parse fail %d", rc);
|
||||
goto fail;
|
||||
}
|
||||
mbedtls_ssl_conf_ca_chain(&t->conf, &t->ca, NULL);
|
||||
mbedtls_ssl_conf_authmode(&t->conf, MBEDTLS_SSL_VERIFY_REQUIRED);
|
||||
mbedtls_ssl_conf_rng(&t->conf, mbedtls_ctr_drbg_random, &t->rng);
|
||||
/* TLS 1.2 ceiling (ported from 1.0, proven on hardware): mbedTLS 3.x
|
||||
* routes TLS 1.3 through the PSA crypto subsystem, whose init fails
|
||||
* on-console (PSA_ERROR_INSUFFICIENT_ENTROPY) and kills every
|
||||
* handshake instantly. TLS 1.2 needs no PSA state. */
|
||||
mbedtls_ssl_conf_max_tls_version(&t->conf, MBEDTLS_SSL_VERSION_TLS1_2);
|
||||
#ifdef MBEDTLS_DEBUG_C
|
||||
mbedtls_debug_set_threshold(3);
|
||||
mbedtls_ssl_conf_dbg(&t->conf, tls_mbedtls_dbg, NULL);
|
||||
#endif
|
||||
{
|
||||
static const char *protos[] = { "http/1.1", NULL };
|
||||
mbedtls_ssl_conf_alpn_protocols(&t->conf, protos);
|
||||
}
|
||||
if((rc = mbedtls_ssl_setup(&t->ssl, &t->conf)) != 0){
|
||||
log_msg("tls: setup fail %d", rc);
|
||||
goto fail;
|
||||
}
|
||||
if((rc = mbedtls_ssl_set_hostname(&t->ssl, host)) != 0){
|
||||
log_msg("tls: hostname fail %d", rc);
|
||||
goto fail;
|
||||
}
|
||||
mbedtls_ssl_set_bio(&t->ssl, t, net_send, net_recv, NULL);
|
||||
|
||||
log_msg("tls: handshake start");
|
||||
{
|
||||
int64_t dl = orbis_mono_s() + 15;
|
||||
extern int daemon_stop_requested(void) __attribute__((weak));
|
||||
for(;;){
|
||||
if(daemon_stop_requested && daemon_stop_requested()){ log_msg("tls: aborted (stop)"); goto fail; }
|
||||
rc = mbedtls_ssl_handshake(&t->ssl);
|
||||
if(rc == 0) break;
|
||||
if(rc != MBEDTLS_ERR_SSL_WANT_READ &&
|
||||
rc != MBEDTLS_ERR_SSL_WANT_WRITE){
|
||||
log_msg("tls: handshake fail %d (mbedTLS 0x%08x)", rc, (unsigned)rc);
|
||||
goto fail;
|
||||
}
|
||||
if(orbis_mono_s() > dl){ log_msg("tls: handshake timeout"); goto fail; }
|
||||
usleep(20000);
|
||||
}
|
||||
}
|
||||
log_msg("tls: established (%s)", mbedtls_ssl_get_version(&t->ssl));
|
||||
{
|
||||
uint32_t vf = mbedtls_ssl_get_verify_result(&t->ssl);
|
||||
if(vf != 0){
|
||||
log_msg("tls: cert verify fail flags=0x%08x", vf);
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
return t;
|
||||
|
||||
fail:
|
||||
mbedtls_ssl_free(&t->ssl);
|
||||
mbedtls_ssl_config_free(&t->conf);
|
||||
mbedtls_x509_crt_free(&t->ca);
|
||||
mbedtls_ctr_drbg_free(&t->rng);
|
||||
mbedtls_entropy_free(&t->ent);
|
||||
free(t);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int tls_read(tls_ctx_t *t, void *buf, size_t cap){
|
||||
if(!t || !buf || cap == 0) return -1;
|
||||
int r = mbedtls_ssl_read(&t->ssl, (unsigned char *)buf, cap);
|
||||
if(r > 0) return r;
|
||||
if(r == MBEDTLS_ERR_SSL_WANT_READ || r == MBEDTLS_ERR_SSL_WANT_WRITE)
|
||||
return 0; /* nothing yet */
|
||||
if(r == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY || r == 0) return -1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int tls_write(tls_ctx_t *t, const void *buf, size_t len){
|
||||
if(!t || (!buf && len)) return -1;
|
||||
const unsigned char *p = (const unsigned char *)buf;
|
||||
size_t done = 0;
|
||||
int64_t dl = orbis_mono_s() + 10;
|
||||
while(done < len){
|
||||
int r = mbedtls_ssl_write(&t->ssl, p + done, len - done);
|
||||
if(r > 0){ done += (size_t)r; continue; }
|
||||
if(r != MBEDTLS_ERR_SSL_WANT_READ && r != MBEDTLS_ERR_SSL_WANT_WRITE){
|
||||
log_msg("tls: write fail %d", r);
|
||||
return -1;
|
||||
}
|
||||
if(orbis_mono_s() > dl){ log_msg("tls: write timeout"); return -1; }
|
||||
usleep(10000);
|
||||
}
|
||||
return (int)done;
|
||||
}
|
||||
|
||||
void tls_free(tls_ctx_t *t){
|
||||
if(!t) return;
|
||||
mbedtls_ssl_close_notify(&t->ssl);
|
||||
mbedtls_ssl_free(&t->ssl);
|
||||
mbedtls_ssl_config_free(&t->conf);
|
||||
mbedtls_x509_crt_free(&t->ca);
|
||||
mbedtls_ctr_drbg_free(&t->rng);
|
||||
mbedtls_entropy_free(&t->ent);
|
||||
free(t);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/* tls.h - self-contained TLS client (mbedTLS, statically linked).
|
||||
* No PS4 TLS-module dependency: works in payload and plugin processes.
|
||||
* Certificate verification is REQUIRED against the curated bundle;
|
||||
* see ca_bundle_pem.h. */
|
||||
#ifndef TLS_H
|
||||
#define TLS_H
|
||||
#include <stddef.h>
|
||||
|
||||
/* Runs the full TLS handshake over an already-connected socket.
|
||||
* Returns heap context, or NULL on failure (logged). */
|
||||
typedef struct tls_ctx tls_ctx_t;
|
||||
tls_ctx_t *tls_start(int fd, const char *host);
|
||||
|
||||
/* Returns decrypted bytes (>0), 0 if nothing available right now,
|
||||
* <0 on close/error. Non-blocking: never waits for the peer. */
|
||||
int tls_read(tls_ctx_t *t, void *buf, size_t cap);
|
||||
|
||||
/* Writes exactly len bytes; returns len or <0. */
|
||||
int tls_write(tls_ctx_t *t, const void *buf, size_t len);
|
||||
|
||||
void tls_free(tls_ctx_t *t);
|
||||
|
||||
#endif
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
/* tmdb.c - Sony TMDB runtime resolver: plain-HTTP GET + cache.
|
||||
* Transport mirrors ws.c patterns (resolver pool, SNDTIMEO connect cap,
|
||||
* recv deadline). One attempt, no retries: failure falls through. */
|
||||
#include "tmdb.h"
|
||||
#include "clock.h"
|
||||
#include "tmdb_crypto.h"
|
||||
#include "log.h"
|
||||
#include "tls.h"
|
||||
#include "updater_http.h"
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#else
|
||||
#include <orbis/Net.h>
|
||||
#include <orbis/Sysmodule.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/socket.h>
|
||||
#endif
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#define TMDB_HOST "tmdb.np.dl.playstation.net"
|
||||
#define TMDB_PORT 80
|
||||
#define TMDB_BODY_MAX 65536
|
||||
#define TMDB_DEADLINE_S 10
|
||||
#define TMDB_HTTPS_DEADLINE_S 12
|
||||
|
||||
static int32_t s_pool = -1;
|
||||
static int s_ready = 0;
|
||||
/* Mirror ws.c net_ensure exactly: sysmodule first, static-once guard,
|
||||
* 128KB pool (4KB starves the resolver), and deliberately NO NetCtl
|
||||
* init inside game processes. */
|
||||
static int net_up(void){
|
||||
if(s_ready) return 0;
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
/* BSD sockets need no init. */
|
||||
s_ready = 1;
|
||||
return 0;
|
||||
#else
|
||||
uint32_t ur = sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_NET);
|
||||
if((int)ur < 0){ log_msg("tmdb: load NET fail %d", (int)ur); return -1; }
|
||||
if(sceNetInit() < 0){ log_msg("tmdb: sceNetInit fail"); return -1; }
|
||||
s_pool = (int32_t)sceNetPoolCreate("tmdb", 128*1024, 0);
|
||||
if(s_pool < 0){ log_msg("tmdb: net pool fail %d", (int)s_pool); return -1; }
|
||||
s_ready = 1;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Minimal blocking HTTP GET with deadline. Returns body bytes, or -1. */
|
||||
static int http_get(const char *host, const char *path,
|
||||
char *body, size_t cap, int *out_status){
|
||||
if(out_status) *out_status = 0;
|
||||
if(net_up() < 0) return -1;
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
struct addrinfo hints, *res = NULL;
|
||||
memset(&hints, 0, sizeof hints);
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
{
|
||||
char portbuf[16];
|
||||
snprintf(portbuf, sizeof portbuf, "%d", TMDB_PORT);
|
||||
if(getaddrinfo(host, portbuf, &hints, &res) != 0 || !res){
|
||||
log_msg("tmdb: dns fail");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if(fd < 0){ log_msg("tmdb: socket fail"); freeaddrinfo(res); return -1; }
|
||||
struct timeval tv = { .tv_sec = TMDB_DEADLINE_S, .tv_usec = 0 };
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
||||
if(connect(fd, res->ai_addr, res->ai_addrlen) < 0){
|
||||
log_msg("tmdb: connect fail"); freeaddrinfo(res); close(fd); return -1;
|
||||
}
|
||||
freeaddrinfo(res);
|
||||
#else
|
||||
OrbisNetInAddr in;
|
||||
memset(&in, 0, sizeof in);
|
||||
int ok = 0;
|
||||
if(s_pool >= 0){
|
||||
int32_t rid = sceNetResolverCreate("tmdbR", (uint32_t)s_pool, 0);
|
||||
if(rid >= 0){
|
||||
if(sceNetResolverStartNtoa(rid, host, &in, 5000000, 3, 0) >= 0) ok = 1;
|
||||
sceNetResolverDestroy(rid);
|
||||
}
|
||||
}
|
||||
if(!ok){
|
||||
/* IP literal fallback */
|
||||
unsigned a, b, c, d;
|
||||
if(sscanf(host, "%u.%u.%u.%u", &a, &b, &c, &d) != 4) return -1;
|
||||
if(a > 255 || b > 255 || c > 255 || d > 255) return -1;
|
||||
uint32_t ip = (uint32_t)((a<<24)|(b<<16)|(c<<8)|d);
|
||||
in.s_addr = sceNetHtonl(ip);
|
||||
}
|
||||
int fd = sceNetSocket("tmdb", ORBIS_NET_AF_INET, ORBIS_NET_SOCK_STREAM, 0);
|
||||
if(fd < 0){ log_msg("tmdb: socket fail"); return -1; }
|
||||
struct timeval tv = { .tv_sec = TMDB_DEADLINE_S, .tv_usec = 0 };
|
||||
sceNetSetsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||
sceNetSetsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
||||
OrbisNetSockaddr sa;
|
||||
memset(&sa, 0, sizeof sa);
|
||||
sa.sa_family = 2;
|
||||
*(uint16_t*)sa.sa_data = sceNetHtons((uint16_t)TMDB_PORT);
|
||||
memcpy(sa.sa_data+2, &in.s_addr, 4);
|
||||
if(sceNetConnect(fd, &sa, sizeof sa) < 0){
|
||||
log_msg("tmdb: connect fail"); sceNetSocketClose(fd); return -1;
|
||||
}
|
||||
#endif
|
||||
char req[512];
|
||||
int rl = snprintf(req, sizeof req,
|
||||
"GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: Mozilla/5.0\r\nConnection: close\r\n\r\n",
|
||||
path, host);
|
||||
if(rl <= 0 || rl >= (int)sizeof req){
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
close(fd);
|
||||
#else
|
||||
sceNetSocketClose(fd);
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
int sent = 0;
|
||||
int64_t dl = orbis_mono_s() + TMDB_DEADLINE_S;
|
||||
while(sent < rl){
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
int r = (int)send(fd, req+sent, (size_t)(rl-sent), 0);
|
||||
if(r > 0){ sent += r; continue; }
|
||||
if(orbis_mono_s() > dl || r == 0){ close(fd); return -1; }
|
||||
#else
|
||||
int r = sceNetSend(fd, req+sent, rl-sent, 0);
|
||||
if(r > 0){ sent += r; continue; }
|
||||
if(orbis_mono_s() > dl || r == 0){ sceNetSocketClose(fd); return -1; }
|
||||
#endif
|
||||
}
|
||||
/* read headers then body; cap total */
|
||||
char hb[2048];
|
||||
size_t hl = 0;
|
||||
int hdr_done = 0, status = 0;
|
||||
size_t bl = 0;
|
||||
for(;;){
|
||||
char tmp[1024];
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
int r = (int)recv(fd, tmp, sizeof tmp, 0);
|
||||
#else
|
||||
int r = sceNetRecv(fd, tmp, sizeof tmp, 0);
|
||||
#endif
|
||||
if(r <= 0) break;
|
||||
size_t off = 0;
|
||||
if(!hdr_done){
|
||||
size_t room = sizeof hb - 1 - hl;
|
||||
size_t cp = (size_t)r < room ? (size_t)r : room;
|
||||
memcpy(hb+hl, tmp, cp); hl += cp; hb[hl] = 0;
|
||||
char *e = strstr(hb, "\r\n\r\n");
|
||||
if(e){
|
||||
hdr_done = 1;
|
||||
if(!strncmp(hb, "HTTP/1.", 7)) status = atoi(hb+9);
|
||||
off = (size_t)(e + 4 - hb) - (hl - cp);
|
||||
/* off = body bytes already in this chunk */
|
||||
if(off > (size_t)r) off = (size_t)r;
|
||||
} else if(hl >= sizeof hb - 1){
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
close(fd); return -1; /* headers too big */
|
||||
#else
|
||||
sceNetSocketClose(fd); return -1; /* headers too big */
|
||||
#endif
|
||||
} else continue;
|
||||
}
|
||||
while(off < (size_t)r && bl < cap - 1){
|
||||
body[bl++] = tmp[off++];
|
||||
}
|
||||
if(bl >= cap - 1) break;
|
||||
if(orbis_mono_s() > dl) break;
|
||||
}
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
close(fd);
|
||||
#else
|
||||
sceNetSocketClose(fd);
|
||||
#endif
|
||||
body[bl] = 0;
|
||||
if(out_status) *out_status = status;
|
||||
if(status != 200 || bl == 0) return -1;
|
||||
return (int)bl;
|
||||
}
|
||||
/* 8-entry cache: one lookup per title per process lifetime. */
|
||||
#define TMDB_CACHE_N 8
|
||||
static struct { char id[16]; char name[128]; char icon[256]; } s_cache[TMDB_CACHE_N];
|
||||
static int s_cache_n = 0;
|
||||
|
||||
|
||||
/* HTTPS GET over TLS (port 443) for the same TMDB paths. Uses the shared
|
||||
* chunked-aware HTTP parser. Returns body bytes or <=0. */
|
||||
static int https_get_tmdb(const char *path, char *out, size_t cap, int *out_status){
|
||||
if(out_status) *out_status = 0;
|
||||
if(!path || !out || cap < 2) return -1;
|
||||
if(net_up() < 0) return -1;
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
struct addrinfo hints, *res = NULL;
|
||||
memset(&hints, 0, sizeof hints);
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
if(getaddrinfo(TMDB_HOST, "443", &hints, &res) != 0 || !res){
|
||||
log_msg("tmdb: https dns fail");
|
||||
return -1;
|
||||
}
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if(fd < 0){ freeaddrinfo(res); return -1; }
|
||||
struct timeval tv = { .tv_sec = TMDB_HTTPS_DEADLINE_S, .tv_usec = 0 };
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||
if(connect(fd, res->ai_addr, res->ai_addrlen) < 0){
|
||||
log_msg("tmdb: https connect fail");
|
||||
freeaddrinfo(res); close(fd); return -1;
|
||||
}
|
||||
freeaddrinfo(res);
|
||||
{
|
||||
int fl = fcntl(fd, F_GETFL, 0);
|
||||
if(fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
||||
}
|
||||
tls_ctx_t *t = tls_start(fd, TMDB_HOST);
|
||||
if(!t){
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
char req[512];
|
||||
int rl = snprintf(req, sizeof req,
|
||||
"GET %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: Mozilla/5.0\r\nConnection: close\r\n\r\n",
|
||||
path, TMDB_HOST);
|
||||
if(rl <= 0 || rl >= (int)sizeof req){ tls_free(t); close(fd); return -1; }
|
||||
if(tls_write(t, req, (size_t)rl) < 0){ tls_free(t); close(fd); return -1; }
|
||||
static char raw[TMDB_BODY_MAX];
|
||||
size_t bl = 0;
|
||||
int64_t dl = orbis_mono_s() + TMDB_HTTPS_DEADLINE_S + 10;
|
||||
for(;;){
|
||||
char tmp[1024];
|
||||
int r = tls_read(t, tmp, sizeof tmp - 1);
|
||||
if(r < 0) break;
|
||||
if(r == 0){
|
||||
if(orbis_mono_s() > dl) break;
|
||||
usleep(20000);
|
||||
continue;
|
||||
}
|
||||
if(bl + (size_t)r >= sizeof raw) break;
|
||||
memcpy(raw + bl, tmp, (size_t)r);
|
||||
bl += (size_t)r;
|
||||
if(orbis_mono_s() > dl) break;
|
||||
}
|
||||
tls_free(t);
|
||||
close(fd);
|
||||
if(bl == 0) return -1;
|
||||
int st = 0;
|
||||
size_t olen = 0;
|
||||
char *body = upd_parse_response(raw, bl, cap, &st, &olen, NULL, 0);
|
||||
if(out_status) *out_status = st;
|
||||
if(!body || st != 200){ free(body); return -1; }
|
||||
if(olen >= cap){ free(body); return -1; }
|
||||
memcpy(out, body, olen);
|
||||
out[olen] = 0;
|
||||
free(body);
|
||||
return (int)olen;
|
||||
#else
|
||||
return -1; /* OpenOrbis app builds keep the plain-HTTP path only. */
|
||||
#endif
|
||||
}
|
||||
|
||||
int tmdb_resolve(const char *titleId, char *name, size_t name_cap,
|
||||
char *icon, size_t icon_cap){
|
||||
if(!titleId || !name || name_cap == 0) return -1;
|
||||
name[0] = 0; if(icon && icon_cap) icon[0] = 0;
|
||||
for(int i = 0; i < s_cache_n; i++){
|
||||
if(!strcmp(s_cache[i].id, titleId)){
|
||||
strncpy(name, s_cache[i].name, name_cap-1); name[name_cap-1] = 0;
|
||||
if(icon && icon_cap){ strncpy(icon, s_cache[i].icon, icon_cap-1); icon[icon_cap-1] = 0; }
|
||||
return name[0] ? 0 : -1;
|
||||
}
|
||||
}
|
||||
/* Live Sony CDN first (TMDB over TLS; plain HTTP is blocked
|
||||
* on-console and handled inside http_get/https fallback below).
|
||||
* No baked tables: CUSA code + Sony CDN is the source of truth. */
|
||||
char path[128];
|
||||
if(tmdb_path(titleId, path, sizeof path) != 0) return -1;
|
||||
static char body[TMDB_BODY_MAX];
|
||||
int status = 0;
|
||||
int n = http_get(TMDB_HOST, path, body, sizeof body, &status);
|
||||
if(n <= 0){
|
||||
/* Port 80 is blocked from jailbroken consoles; Sony also answers
|
||||
* the same paths over TLS (443). Try HTTPS before giving up so
|
||||
* new installs resolve live like everything else. */
|
||||
n = https_get_tmdb(path, body, sizeof body, &status);
|
||||
if(n > 0) log_msg("tmdb: live via https for %s", titleId);
|
||||
}
|
||||
if(n <= 0){ log_msg("tmdb: fetch fail status=%d", status); return -1; }
|
||||
char iname[128] = "", iicon[256] = "";
|
||||
if(tmdb_parse(body, (size_t)n, iname, sizeof iname, iicon, sizeof iicon) != 0){
|
||||
log_msg("tmdb: parse fail for %s", titleId);
|
||||
return -1;
|
||||
}
|
||||
strncpy(name, iname, name_cap-1); name[name_cap-1] = 0;
|
||||
if(icon && icon_cap){ strncpy(icon, iicon, icon_cap-1); icon[icon_cap-1] = 0; }
|
||||
if(s_cache_n < TMDB_CACHE_N){
|
||||
strncpy(s_cache[s_cache_n].id, titleId, 15);
|
||||
strncpy(s_cache[s_cache_n].name, iname, 127);
|
||||
strncpy(s_cache[s_cache_n].icon, iicon, 255);
|
||||
s_cache_n++;
|
||||
}
|
||||
log_msg("name: %s via tmdb", name);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/* tmdb.h - Sony TMDB runtime title/icon resolver (PS4 transport + cache). */
|
||||
#ifndef TMDB_H
|
||||
#define TMDB_H
|
||||
#include <stddef.h>
|
||||
/* Resolve titleId (e.g. "CUSA00740") to official display name and icon
|
||||
* URL via Sony's TMDB service. Results cached in memory. Returns 0 with
|
||||
* name set on success (icon may stay empty), -1 on any failure.
|
||||
* Bounded: single attempt, ~10s worst case, no retries (caller falls
|
||||
* through to the next name source). */
|
||||
int tmdb_resolve(const char *titleId, char *name, size_t name_cap,
|
||||
char *icon, size_t icon_cap);
|
||||
#endif
|
||||
@@ -0,0 +1,136 @@
|
||||
/* tmdb_crypto.c - self-contained SHA1 + HMAC-SHA1 + Sony TMDB URL builder
|
||||
* and response parser. No platform headers: host-unit-testable.
|
||||
*
|
||||
* Name/icon resolution idea follows bshar1865/zorua98741
|
||||
* PS4-Rich-Presence-for-Discord (TMDB key/hash scheme, Tustin's hash
|
||||
* construction): title metadata comes from Sony's own
|
||||
* tmdb.np.dl.playstation.net service. Our implementation here (parser,
|
||||
* transport, caching, presence wiring) is original. */
|
||||
#include "tmdb_crypto.h"
|
||||
#include "jsonlite.h"
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* ---- SHA1 (small, public-domain style) ---- */
|
||||
typedef struct { uint32_t h[5]; uint64_t len; unsigned char buf[64]; size_t n; } sha1_t;
|
||||
static uint32_t rol(uint32_t x, int n){ return (x<<n)|(x>>(32-n)); }
|
||||
static void sha1_block(sha1_t *c, const unsigned char *p){
|
||||
uint32_t w[80];
|
||||
for(int i=0;i<16;i++) w[i]=(uint32_t)p[4*i]<<24|(uint32_t)p[4*i+1]<<16|(uint32_t)p[4*i+2]<<8|p[4*i+3];
|
||||
for(int i=16;i<80;i++) w[i]=rol(w[i-3]^w[i-8]^w[i-14]^w[i-16],1);
|
||||
uint32_t a=c->h[0],b=c->h[1],d=c->h[2],e=c->h[3],f=c->h[4];
|
||||
for(int i=0;i<80;i++){
|
||||
uint32_t t;
|
||||
if(i<20) t=((b&d)|((~b)&e))+0x5A827999u;
|
||||
else if(i<40) t=(b^d^e)+0x6ED9EBA1u;
|
||||
else if(i<60) t=((b&d)|(b&e)|(d&e))+0x8F1BBCDCu;
|
||||
else t=(b^d^e)+0xCA62C1D6u;
|
||||
t+=rol(a,5)+f+w[i]; f=e; e=d; d=rol(b,30); b=a; a=t;
|
||||
}
|
||||
c->h[0]+=a; c->h[1]+=b; c->h[2]+=d; c->h[3]+=e; c->h[4]+=f;
|
||||
}
|
||||
static void sha1_init(sha1_t *c){
|
||||
c->h[0]=0x67452301u; c->h[1]=0xEFCDAB89u; c->h[2]=0x98BADCFEu;
|
||||
c->h[3]=0x10325476u; c->h[4]=0xC3D2E1F0u; c->len=0; c->n=0;
|
||||
}
|
||||
static void sha1_update(sha1_t *c, const unsigned char *p, size_t n){
|
||||
c->len+=n;
|
||||
while(n){
|
||||
size_t t=64-c->n; if(t>n) t=n;
|
||||
memcpy(c->buf+c->n,p,t); c->n+=t; p+=t; n-=t;
|
||||
if(c->n==64){ sha1_block(c,c->buf); c->n=0; }
|
||||
}
|
||||
}
|
||||
static void sha1_final(sha1_t *c, unsigned char out[20]){
|
||||
uint64_t bits=c->len*8;
|
||||
unsigned char z=0x80, zz=0x00;
|
||||
sha1_update(c,&z,1);
|
||||
while(c->n!=56) sha1_update(c,&zz,1);
|
||||
unsigned char lb[8];
|
||||
for(int i=0;i<8;i++) lb[i]=(unsigned char)(bits>>((7-i)*8));
|
||||
memcpy(c->buf+c->n,lb,8); sha1_block(c,c->buf);
|
||||
for(int i=0;i<5;i++){
|
||||
out[4*i]=(unsigned char)(c->h[i]>>24); out[4*i+1]=(unsigned char)(c->h[i]>>16);
|
||||
out[4*i+2]=(unsigned char)(c->h[i]>>8); out[4*i+3]=(unsigned char)c->h[i];
|
||||
}
|
||||
}
|
||||
void tmdb_sha1(const unsigned char *p, size_t n, unsigned char out[20]){
|
||||
sha1_t c; sha1_init(&c); sha1_update(&c,p,n); sha1_final(&c,out);
|
||||
}
|
||||
void tmdb_hmac_sha1(const unsigned char *key, size_t klen,
|
||||
const unsigned char *msg, size_t mlen,
|
||||
unsigned char out[20]){
|
||||
unsigned char k[64];
|
||||
if(klen>64){ tmdb_sha1(key,klen,k); memset(k+20,0,44); klen=20; }
|
||||
else { memcpy(k,key,klen); memset(k+klen,0,64-klen); }
|
||||
unsigned char ipad[64], opad[64];
|
||||
for(int i=0;i<64;i++){ ipad[i]=k[i]^0x36; opad[i]=k[i]^0x5C; }
|
||||
sha1_t c; unsigned char inner[20];
|
||||
sha1_init(&c); sha1_update(&c,ipad,64); sha1_update(&c,msg,mlen); sha1_final(&c,inner);
|
||||
sha1_init(&c); sha1_update(&c,opad,64); sha1_update(&c,inner,20); sha1_final(&c,out);
|
||||
}
|
||||
|
||||
/* Sony TMDB service key (same key their own tooling uses). */
|
||||
static const unsigned char tmdb_key[64] = {
|
||||
0xF5,0xDE,0x66,0xD2,0x68,0x0E,0x25,0x5B,0x2D,0xF7,0x9E,0x74,0xF8,0x90,0xEB,0xF3,
|
||||
0x49,0x26,0x2F,0x61,0x8B,0xCA,0xE2,0xA9,0xAC,0xCD,0xEE,0x51,0x56,0xCE,0x8D,0xF2,
|
||||
0xCD,0xF2,0xD4,0x8C,0x71,0x17,0x3C,0xDC,0x25,0x94,0x46,0x5B,0x87,0x40,0x5D,0x19,
|
||||
0x7C,0xF1,0xAE,0xD3,0xB7,0xE9,0x67,0x1E,0xEB,0x56,0xCA,0x67,0x53,0xC2,0xE6,0xB0
|
||||
};
|
||||
|
||||
/* Path (no scheme/host): /tmdb2/<TID>_00_<UPPERHEX>/<TID>_00.json Const-size. */
|
||||
int tmdb_path(const char *titleId, char *out, size_t cap){
|
||||
static const char hex[] = "0123456789ABCDEF";
|
||||
char tid[16];
|
||||
size_t i = 0;
|
||||
if(!titleId || !out || cap == 0) return -1;
|
||||
while(titleId[i] && i < sizeof tid - 1){
|
||||
char c = titleId[i];
|
||||
if(!((c>='A'&&c<='Z')||(c>='0'&&c<='9'))) return -1;
|
||||
tid[i] = c; i++;
|
||||
}
|
||||
tid[i] = 0;
|
||||
if(i != 9) return -1;
|
||||
char msg[16];
|
||||
memcpy(msg, tid, 9); memcpy(msg+9, "_00", 4); /* includes null */
|
||||
unsigned char mac[20];
|
||||
tmdb_hmac_sha1(tmdb_key, sizeof tmdb_key, (unsigned char*)msg, 12, mac);
|
||||
/* need: /tmdb2/XXXXXXXXX_00_40HEX/XXXXXXXXX_00.json = 7+9+3+40+1+9+5+1 */
|
||||
if(cap < 80) return -1;
|
||||
int n = snprintf(out, cap, "/tmdb2/%s_00_", tid);
|
||||
for(int k = 0; k < 20; k++){ out[n++] = hex[mac[k]>>4]; out[n++] = hex[mac[k]&15]; }
|
||||
n += snprintf(out+n, cap-(size_t)n, "/%s_00.json", tid);
|
||||
(void)n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Parse a TMDB JSON body: names[0].name + icons[0].icon. */
|
||||
int tmdb_parse(const char *body, size_t len,
|
||||
char *name, size_t name_cap, char *icon, size_t icon_cap){
|
||||
if(!body || !name || name_cap == 0) return -1;
|
||||
name[0] = 0; if(icon && icon_cap) icon[0] = 0;
|
||||
jl_val_t *r = jl_parse(body, len);
|
||||
if(!r) return -1;
|
||||
int rc = -1;
|
||||
const jl_val_t *names = jl_obj_get(r, "names");
|
||||
const jl_val_t *n0 = (names && names->type == JL_ARRAY) ? jl_arr_at(names, 0) : NULL;
|
||||
const jl_val_t *nm = n0 ? jl_obj_get(n0, "name") : NULL;
|
||||
if(nm && nm->type == JL_STRING && nm->str[0]){
|
||||
strncpy(name, nm->str, name_cap-1); name[name_cap-1] = 0;
|
||||
rc = 0;
|
||||
if(icon && icon_cap){
|
||||
const jl_val_t *icons = jl_obj_get(r, "icons");
|
||||
const jl_val_t *i0 = (icons && icons->type == JL_ARRAY) ? jl_arr_at(icons, 0) : NULL;
|
||||
const jl_val_t *iu = i0 ? jl_obj_get(i0, "icon") : NULL;
|
||||
if(iu && iu->type == JL_STRING && iu->str[0]){
|
||||
/* only http(s) URLs are usable as external assets */
|
||||
if(!strncmp(iu->str, "http://", 7) || !strncmp(iu->str, "https://", 8)){
|
||||
strncpy(icon, iu->str, icon_cap-1); icon[icon_cap-1] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
jl_free(r);
|
||||
return rc;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/* tmdb_crypto.h - Sony TMDB URL builder + response parser (host-testable). */
|
||||
#ifndef TMDB_CRYPTO_H
|
||||
#define TMDB_CRYPTO_H
|
||||
#include <stddef.h>
|
||||
void tmdb_sha1(const unsigned char *p, size_t n, unsigned char out[20]);
|
||||
void tmdb_hmac_sha1(const unsigned char *key, size_t klen,
|
||||
const unsigned char *msg, size_t mlen,
|
||||
unsigned char out[20]);
|
||||
/* Build "/tmdb2/<TID>_00_<HEX>/<TID>_00.json". 0 ok, -1 invalid input. */
|
||||
int tmdb_path(const char *titleId, char *out, size_t cap);
|
||||
/* Parse TMDB JSON body into display name + icon URL. 0 ok, -1 no name. */
|
||||
int tmdb_parse(const char *body, size_t len,
|
||||
char *name, size_t name_cap, char *icon, size_t icon_cap);
|
||||
#endif
|
||||
@@ -0,0 +1,528 @@
|
||||
/* updater.c - self-updater. Pure logic (host-tested) plus PS4 HTTPS.
|
||||
*
|
||||
* Flow, once per daemon boot when enabled:
|
||||
* GET https://api.github.com/repos/<repo>/releases/latest
|
||||
* -> tag_name + asset browser_download_urls
|
||||
* if tag newer than ORBISRPC_VERSION:
|
||||
* download each known asset (cap 4MB), validate ELF magic,
|
||||
* write <target>.new, rename over target.
|
||||
* Plugin .prx takes effect on next game launch; payload .bin on next
|
||||
* injection. Never deletes, never writes unvalidated bytes.
|
||||
*/
|
||||
#include "updater.h"
|
||||
#include "updater_http.h"
|
||||
#include "version.h"
|
||||
#include "clock.h"
|
||||
#include "jsonlite.h"
|
||||
#include "health.h"
|
||||
#include "manifest.h"
|
||||
#include "release_pubkey.h"
|
||||
#include "log.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* ---- PS4 HTTPS transport (mirrors ws.c/tmdb.c bring-up) ---- */
|
||||
#include "tls.h"
|
||||
#include <mbedtls/sha256.h>
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <fcntl.h>
|
||||
#else
|
||||
#include <orbis/Net.h>
|
||||
#include <orbis/Sysmodule.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/socket.h>
|
||||
#endif
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define UPD_HOST "api.github.com"
|
||||
#define UPD_DEADLINE_S 15
|
||||
#define UPD_BODY_MAX (4u*1024u*1024u)
|
||||
#define UPD_MAX_REDIRECTS 3
|
||||
|
||||
/* Release-channel host allowlist: only GitHub API + release infra.
|
||||
* Redirects anywhere else are refused (fail closed). */
|
||||
static int upd_host_allowed(const char *host){
|
||||
static const char *ok[] = {
|
||||
"api.github.com",
|
||||
"github.com",
|
||||
"uploads.github.com",
|
||||
"objects.githubusercontent.com",
|
||||
"codeload.github.com",
|
||||
"raw.githubusercontent.com",
|
||||
NULL,
|
||||
};
|
||||
if(!host || !host[0]) return 0;
|
||||
for(int i = 0; ok[i]; i++) if(!strcmp(host, ok[i])) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifndef SO_NBIO
|
||||
#define SO_NBIO 0x2000 /* same local define as ws.c; absent from SDK headers */
|
||||
#endif
|
||||
|
||||
static int32_t s_upool = -1;
|
||||
static int upd_net(void){
|
||||
static int ready = 0;
|
||||
if(ready) return 0;
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
ready = 1;
|
||||
return 0;
|
||||
#else
|
||||
uint32_t ur = sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_NET);
|
||||
if((int)ur < 0) return -1;
|
||||
if(sceNetInit() < 0) return -1;
|
||||
s_upool = (int32_t)sceNetPoolCreate("upd", 128*1024, 0);
|
||||
if(s_upool < 0) return -1;
|
||||
ready = 1;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
static int upd_connect(const char *host, int port){
|
||||
if(upd_net() < 0) return -1;
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
struct addrinfo hints, *res = NULL;
|
||||
memset(&hints, 0, sizeof hints);
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
{
|
||||
char portbuf[16];
|
||||
snprintf(portbuf, sizeof portbuf, "%d", port);
|
||||
if(getaddrinfo(host, portbuf, &hints, &res) != 0 || !res){
|
||||
log_msg("updater: dns fail");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if(fd < 0){ freeaddrinfo(res); return -1; }
|
||||
struct timeval tv = { .tv_sec = UPD_DEADLINE_S, .tv_usec = 0 };
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||
if(connect(fd, res->ai_addr, res->ai_addrlen) < 0){
|
||||
close(fd); freeaddrinfo(res); return -1;
|
||||
}
|
||||
freeaddrinfo(res);
|
||||
{
|
||||
int fl = fcntl(fd, F_GETFL, 0);
|
||||
if(fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
||||
}
|
||||
return fd;
|
||||
#else
|
||||
OrbisNetInAddr in;
|
||||
memset(&in, 0, sizeof in);
|
||||
int32_t rid = sceNetResolverCreate("updR", (uint32_t)s_upool, 0);
|
||||
if(rid < 0) return -1;
|
||||
int32_t rr = sceNetResolverStartNtoa(rid, host, &in, 8000000, 3, 0);
|
||||
sceNetResolverDestroy(rid);
|
||||
if(rr < 0){ log_msg("updater: dns fail"); return -1; }
|
||||
int fd = sceNetSocket("upd", ORBIS_NET_AF_INET, ORBIS_NET_SOCK_STREAM, 0);
|
||||
if(fd < 0) return -1;
|
||||
struct timeval tv = { .tv_sec = UPD_DEADLINE_S, .tv_usec = 0 };
|
||||
sceNetSetsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||
OrbisNetSockaddr sa;
|
||||
memset(&sa, 0, sizeof sa);
|
||||
sa.sa_family = 2;
|
||||
*(uint16_t*)sa.sa_data = sceNetHtons((uint16_t)port);
|
||||
memcpy(sa.sa_data+2, &in.s_addr, 4);
|
||||
if(sceNetConnect(fd, &sa, sizeof sa) < 0){
|
||||
sceNetSocketClose(fd); return -1;
|
||||
}
|
||||
int nb = 1;
|
||||
sceNetSetsockopt(fd, SOL_SOCKET, SO_NBIO, &nb, sizeof nb);
|
||||
return fd;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* HTTPS GET with real HTTP semantics (bounded):
|
||||
* status line + headers (cap UPD_HDR_MAX) + Content-Length or
|
||||
* Transfer-Encoding: chunked decoding + redirect capture.
|
||||
* Returns heap body (caller frees) with out_len; out_status always set;
|
||||
* out_location gets the redirect target ("" when none). Only the body
|
||||
* counts against cap; headers are separately bounded. */
|
||||
static char *https_get_once(const char *host, const char *path,
|
||||
size_t cap, int *out_status, size_t *out_len,
|
||||
char *out_location, size_t loc_cap){
|
||||
if(out_status) *out_status = 0;
|
||||
if(out_len) *out_len = 0;
|
||||
if(out_location && loc_cap) out_location[0] = 0;
|
||||
if(!upd_host_allowed(host)){ log_msg("updater: host refused (%s)", host); return NULL; }
|
||||
int fd = upd_connect(host, 443);
|
||||
if(fd < 0) return NULL;
|
||||
tls_ctx_t *t = tls_start(fd, host);
|
||||
if(!t){
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
close(fd);
|
||||
#else
|
||||
sceNetSocketClose(fd);
|
||||
#endif
|
||||
return NULL;
|
||||
}
|
||||
char req[512];
|
||||
int rl = snprintf(req, sizeof req,
|
||||
"GET %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: orbisRPC/%s\r\nConnection: close\r\n\r\n",
|
||||
path, host, ORBISRPC_VERSION);
|
||||
if(rl <= 0 || rl >= (int)sizeof req){ tls_free(t); return NULL; }
|
||||
if(tls_write(t, req, (size_t)rl) < 0){ tls_free(t); return NULL; }
|
||||
/* Read raw response (headers + body) up to header cap + body cap. */
|
||||
size_t rawcap = UPD_HDR_MAX + cap;
|
||||
char *raw = (char*)malloc(rawcap);
|
||||
if(!raw){ tls_free(t); return NULL; }
|
||||
size_t rl2 = 0;
|
||||
int64_t dl = orbis_mono_s() + UPD_DEADLINE_S + 20;
|
||||
for(;;){
|
||||
char tmp[2048];
|
||||
int r = tls_read(t, tmp, sizeof tmp);
|
||||
if(r < 0) break;
|
||||
if(r == 0){
|
||||
if(orbis_mono_s() > dl) break;
|
||||
usleep(20000);
|
||||
continue;
|
||||
}
|
||||
size_t room = rawcap > rl2 ? rawcap - rl2 : 0;
|
||||
size_t cp = (size_t)r < room ? (size_t)r : room;
|
||||
if(cp) memcpy(raw + rl2, tmp, cp);
|
||||
rl2 += cp;
|
||||
if(rl2 >= rawcap - 1 || orbis_mono_s() > dl) break;
|
||||
}
|
||||
tls_free(t);
|
||||
if(rl2 == 0){ free(raw); return NULL; }
|
||||
raw[rl2] = 0;
|
||||
char *out = upd_parse_response(raw, rl2, cap, out_status, out_len,
|
||||
out_location, loc_cap);
|
||||
free(raw);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* HTTPS GET following absolute-URL https redirects within the allowlist
|
||||
* (GitHub release assets redirect to object storage). Relative Location
|
||||
* values stay on the same host. Refuses non-https, off-allowlist, and
|
||||
* redirect loops. */
|
||||
static char *https_get(const char *host, const char *path,
|
||||
size_t cap, int *out_status, size_t *out_len){
|
||||
char hbuf[128], pbuf[512], loc[512];
|
||||
snprintf(hbuf, sizeof hbuf, "%s", host);
|
||||
snprintf(pbuf, sizeof pbuf, "%s", path);
|
||||
for(int i = 0; i <= UPD_MAX_REDIRECTS; i++){
|
||||
int status = 0;
|
||||
size_t len = 0;
|
||||
char *body = https_get_once(hbuf, pbuf, cap, &status, &len,
|
||||
loc, sizeof loc);
|
||||
if(!body){
|
||||
if(out_status) *out_status = status;
|
||||
if(out_len) *out_len = 0;
|
||||
return NULL;
|
||||
}
|
||||
if((status == 301 || status == 302 || status == 303 ||
|
||||
status == 307 || status == 308) && loc[0]){
|
||||
free(body);
|
||||
if(!strncmp(loc, "https://", 8)){
|
||||
const char *h0 = loc + 8;
|
||||
const char *p0 = strchr(h0, '/');
|
||||
if(!p0 || (size_t)(p0 - h0) >= sizeof hbuf){
|
||||
if(out_status) *out_status = status;
|
||||
if(out_len) *out_len = 0;
|
||||
return NULL;
|
||||
}
|
||||
memcpy(hbuf, h0, (size_t)(p0 - h0));
|
||||
hbuf[p0 - h0] = 0;
|
||||
snprintf(pbuf, sizeof pbuf, "%s", p0);
|
||||
} else if(loc[0] == '/'){
|
||||
snprintf(pbuf, sizeof pbuf, "%s", loc);
|
||||
} else {
|
||||
if(out_status) *out_status = status;
|
||||
if(out_len) *out_len = 0;
|
||||
return NULL;
|
||||
}
|
||||
if(!upd_host_allowed(hbuf)){
|
||||
log_msg("updater: redirect refused (off-allowlist %s)", hbuf);
|
||||
if(out_status) *out_status = status;
|
||||
if(out_len) *out_len = 0;
|
||||
return NULL;
|
||||
}
|
||||
log_msg("updater: redirect -> %s%s", hbuf, pbuf);
|
||||
continue;
|
||||
}
|
||||
if(out_status) *out_status = status;
|
||||
if(out_len) *out_len = len;
|
||||
if(status != 200 || len == 0){ free(body); return NULL; }
|
||||
return body;
|
||||
}
|
||||
if(out_status) *out_status = 0;
|
||||
if(out_len) *out_len = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int stage_file(const char *target, const unsigned char *data, size_t n){
|
||||
char tmp[192];
|
||||
snprintf(tmp, sizeof tmp, "%s.new", target);
|
||||
if(!updater_image_ok(data, n)){ log_msg("updater: staged bytes failed validation"); return -1; }
|
||||
FILE *f = fopen(tmp, "wb");
|
||||
if(!f){ log_msg("updater: cannot write %s", tmp); return -1; }
|
||||
int ok = (fwrite(data, 1, n, f) == n);
|
||||
if(fflush(f) != 0) ok = 0;
|
||||
if(ok){ int fd = fileno(f); if(fd >= 0 && fsync(fd) != 0) ok = 0; }
|
||||
if(fclose(f) != 0) ok = 0;
|
||||
if(!ok){ remove(tmp); return -1; }
|
||||
/* Atomic verified activation: validates <target>.new, backs up live
|
||||
* to .bak, activates, re-verifies, auto-restores .bak on failure.
|
||||
* A refused stage leaves the live target untouched (ORX-UPDATE-003). */
|
||||
if(health_stage_activate(target) != 0){
|
||||
log_msg("updater: stage activate failed for %s; live kept", target);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* SHA256 of a buffer, hex-encoded (64 chars + NUL). Returns 0 on success. */
|
||||
static int sha256_hex(const unsigned char *data, size_t n, char out[65]){
|
||||
unsigned char dig[32];
|
||||
mbedtls_sha256_context sc;
|
||||
mbedtls_sha256_init(&sc);
|
||||
int ok = mbedtls_sha256_starts(&sc, 0) == 0 &&
|
||||
mbedtls_sha256_update(&sc, data ? data : (const unsigned char *)"", n) == 0 &&
|
||||
mbedtls_sha256_finish(&sc, dig) == 0;
|
||||
mbedtls_sha256_free(&sc);
|
||||
if(!ok) return -1;
|
||||
for(int i=0;i<32;i++) snprintf(out+2*i, 3, "%02x", dig[i]);
|
||||
out[64]=0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Look up "<filename>" in a SHA256SUMS body ("<hash> <name>\n" lines).
|
||||
* Returns 0 and fills want_hex when found. */
|
||||
static int sums_lookup(const char *sums, const char *filename, char want_hex[65]){
|
||||
size_t fnlen = strlen(filename);
|
||||
const char *p = sums;
|
||||
while(*p){
|
||||
while(*p==' '||*p=='\t'||*p=='\r'||*p=='\n') p++;
|
||||
if(!*p) break;
|
||||
if(strlen(p) < 64) break;
|
||||
char hex[65];
|
||||
memcpy(hex, p, 64); hex[64]=0;
|
||||
int ishex=1;
|
||||
for(int i=0;i<64;i++){
|
||||
char c=hex[i];
|
||||
if(!((c>='0'&&c<='9')||(c>='a'&&c<='f')||(c>='A'&&c<='F'))){ ishex=0; break; }
|
||||
}
|
||||
const char *e = strchr(p, '\n');
|
||||
size_t linelen = e ? (size_t)(e-p) : strlen(p);
|
||||
if(ishex && linelen > 65){
|
||||
const char *nl = p+64;
|
||||
while(nl<p+linelen && (*nl==' '||*nl=='\t'||*nl=='*')) nl++;
|
||||
if((size_t)(p+linelen-(nl)) >= fnlen && !memcmp(nl, filename, fnlen) &&
|
||||
(nl[fnlen]=='\n'||nl[fnlen]=='\r'||nl[fnlen]==' '||nl[fnlen]=='\t'||nl[fnlen]==0||nl[fnlen]=='*')){
|
||||
for(int i=0;i<64;i++){
|
||||
char c=hex[i];
|
||||
want_hex[i]=(c>='A'&&c<='F')?(char)(c-'A'+'a'):c;
|
||||
}
|
||||
want_hex[64]=0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
p = e ? e+1 : p+linelen;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Download a release asset by exact name. Returns heap body or NULL. */
|
||||
static char *fetch_asset(const jl_val_t *assets, const char *want_name,
|
||||
size_t cap, int *status, size_t *out_len){
|
||||
for(size_t i = 0; ; i++){
|
||||
const jl_val_t *a = jl_arr_at(assets, i);
|
||||
if(!a) break;
|
||||
const jl_val_t *nm = jl_obj_get(a, "name");
|
||||
const jl_val_t *dl = jl_obj_get(a, "browser_download_url");
|
||||
if(!nm || nm->type != JL_STRING || !dl || dl->type != JL_STRING) continue;
|
||||
if(strcmp(nm->str, want_name) != 0) continue;
|
||||
const char *url = dl->str;
|
||||
if(strncmp(url, "https://", 8) != 0) return NULL;
|
||||
const char *h0 = url + 8;
|
||||
const char *p0 = strchr(h0, '/');
|
||||
if(!p0 || (size_t)(p0 - h0) >= 128) return NULL;
|
||||
char host[128];
|
||||
memcpy(host, h0, (size_t)(p0 - h0)); host[p0 - h0] = 0;
|
||||
return https_get(host, p0, cap, status, out_len);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Decode manifest.sig: raw 64 bytes, or 128 hex chars. 0 ok. */
|
||||
static int decode_sig(const char *body, size_t len, unsigned char out[64]){
|
||||
if(len == 64){ memcpy(out, body, 64); return 0; }
|
||||
/* strip whitespace for hex form; exactly 128 hex chars required —
|
||||
* trailing garbage after the 128 fails closed */
|
||||
char hex[129];
|
||||
size_t hn = 0, total = 0;
|
||||
for(size_t i = 0; i < len; i++){
|
||||
char c = body[i];
|
||||
if(c == ' ' || c == '\t' || c == '\r' || c == '\n') continue;
|
||||
total++;
|
||||
if(hn < 128) hex[hn++] = c;
|
||||
}
|
||||
if(hn != 128 || total != 128) return -1;
|
||||
for(int i = 0; i < 64; i++){
|
||||
unsigned v = 0;
|
||||
for(int k = 0; k < 2; k++){
|
||||
char c = hex[2 * i + k];
|
||||
v <<= 4;
|
||||
if(c >= '0' && c <= '9') v |= (unsigned)(c - '0');
|
||||
else if(c >= 'a' && c <= 'f') v |= (unsigned)(c - 'a' + 10);
|
||||
else if(c >= 'A' && c <= 'F') v |= (unsigned)(c - 'A' + 10);
|
||||
else return -1;
|
||||
}
|
||||
out[i] = (unsigned char)v;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int updater_check_and_stage(void){
|
||||
char path[160];
|
||||
snprintf(path, sizeof path, "/repos/%s/releases/latest", ORBISRPC_REPO);
|
||||
size_t rl = 0;
|
||||
int status = 0;
|
||||
char *js = https_get(UPD_HOST, path, 65536, &status, &rl);
|
||||
if(!js){ log_msg("updater: release check failed (status=%d)", status); return -1; }
|
||||
jl_val_t *r = jl_parse(js, rl);
|
||||
free(js);
|
||||
if(!r){ log_msg("updater: release parse failed"); return -1; }
|
||||
const jl_val_t *tag = jl_obj_get(r, "tag_name");
|
||||
const jl_val_t *assets = jl_obj_get(r, "assets");
|
||||
int updated = 0;
|
||||
if(tag && tag->type == JL_STRING && tag->str[0] &&
|
||||
updater_cmp(tag->str, ORBISRPC_VERSION) > 0){
|
||||
log_msg("updater: %s available (local %s)", tag->str, ORBISRPC_VERSION);
|
||||
if(assets && assets->type == JL_ARRAY){
|
||||
/* Preferred: signed manifest (manifest.json + manifest.sig).
|
||||
* Verified with the embedded release pubkey; every binary must
|
||||
* match its listed SHA256. A bad signature refuses the whole
|
||||
* update (ORX-UPDATE-002). */
|
||||
manifest_t mf;
|
||||
int have_manifest = 0;
|
||||
size_t ml = 0;
|
||||
char *mbody = fetch_asset(assets, "manifest.json", 65536, &status, &ml);
|
||||
if(mbody){
|
||||
size_t sl = 0;
|
||||
char *sbody = fetch_asset(assets, "manifest.sig", 4096, &status, &sl);
|
||||
if(sbody && manifest_parse(mbody, ml, &mf) == 0 &&
|
||||
manifest_gate(&mf) && manifest_is_newer(&mf, ORBISRPC_VERSION)){
|
||||
unsigned char sig[64];
|
||||
if(decode_sig(sbody, sl, sig) == 0 &&
|
||||
manifest_verify_sig((unsigned char*)mbody, ml, sig,
|
||||
ORBISRPC_RELEASE_PUBKEY) == 0){
|
||||
have_manifest = 1;
|
||||
log_msg("updater: manifest %s verified (key %s)",
|
||||
mf.version, ORBISRPC_RELEASE_KEY_ID);
|
||||
} else {
|
||||
log_msg("updater: WARN ORX-UPDATE-002: manifest signature invalid; refusing update");
|
||||
}
|
||||
} else if(sbody){
|
||||
log_msg("updater: WARN ORX-UPDATE-002: manifest invalid/gated; refusing update");
|
||||
}
|
||||
free(sbody);
|
||||
if(!have_manifest){ free(mbody); mbody = NULL; }
|
||||
}
|
||||
/* Compat fallback: SHA256SUMS (unsigned but hash-pinned). */
|
||||
char *sums = NULL;
|
||||
if(!have_manifest){
|
||||
sums = fetch_asset(assets, "SHA256SUMS", 65536, &status, NULL);
|
||||
if(!sums)
|
||||
log_msg("updater: WARN ORX-UPDATE-002: no manifest and no SHA256SUMS; refusing unsigned update");
|
||||
}
|
||||
/* Two-phase commit: download + verify EVERY asset first, then
|
||||
* activate all at once. A failure anywhere stages nothing, so
|
||||
* the runtime can never mix a new payload with an old plugin
|
||||
* (or vice versa) — rollback restores the complete runtime. */
|
||||
struct { const char *target; char *bin; size_t len; } pend[2];
|
||||
memset(pend, 0, sizeof pend);
|
||||
int pend_n = 0, pend_fail = 0;
|
||||
for(size_t i = 0; ; i++){
|
||||
const jl_val_t *a = jl_arr_at(assets, i);
|
||||
if(!a) break;
|
||||
const jl_val_t *nm = jl_obj_get(a, "name");
|
||||
const jl_val_t *dl = jl_obj_get(a, "browser_download_url");
|
||||
if(!nm || nm->type != JL_STRING || !dl || dl->type != JL_STRING) continue;
|
||||
const char *target = NULL;
|
||||
if(!strcmp(nm->str, "orbisrpc.bin")) target = "/data/payloads/orbisrpc.bin";
|
||||
else if(!strcmp(nm->str, "orbisrpc_plugin.prx")) target = "/data/GoldHEN/plugins/orbisrpc_plugin.prx";
|
||||
else continue;
|
||||
/* download URLs must be https (fail closed on http/other). */
|
||||
const char *url = dl->str;
|
||||
if(strncmp(url, "https://", 8) != 0){ pend_fail = 1; break; }
|
||||
const char *h0 = url + 8;
|
||||
const char *p0 = strchr(h0, '/');
|
||||
if(!p0 || (size_t)(p0-h0) >= 128){ pend_fail = 1; break; }
|
||||
char host[128];
|
||||
memcpy(host, h0, (size_t)(p0-h0)); host[p0-h0] = 0;
|
||||
size_t al = 0;
|
||||
char *bin = https_get(host, p0, UPD_BODY_MAX, &status, &al);
|
||||
if(!bin || !updater_image_ok((unsigned char*)bin, al)){
|
||||
log_msg("updater: asset %s failed validation", nm->str);
|
||||
free(bin);
|
||||
pend_fail = 1;
|
||||
break;
|
||||
}
|
||||
if(have_manifest){
|
||||
if(manifest_check(&mf, nm->str, (unsigned char*)bin, al) != 0){
|
||||
log_msg("updater: asset %s not in manifest or hash mismatch; refusing (ORX-UPDATE-002)", nm->str);
|
||||
free(bin);
|
||||
pend_fail = 1;
|
||||
break;
|
||||
}
|
||||
} else if(sums){
|
||||
char want[65];
|
||||
if(sums_lookup(sums, nm->str, want) != 0){
|
||||
log_msg("updater: asset %s not in SHA256SUMS; refusing", nm->str);
|
||||
free(bin);
|
||||
pend_fail = 1;
|
||||
break;
|
||||
}
|
||||
char got[65];
|
||||
if(sha256_hex((unsigned char*)bin, al, got) != 0 || strcmp(got, want) != 0){
|
||||
log_msg("updater: asset %s hash mismatch; refusing", nm->str);
|
||||
free(bin);
|
||||
pend_fail = 1;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
/* No manifest and no SHA256SUMS: refuse unsigned bytes. */
|
||||
log_msg("updater: asset %s refused: no trust anchor (ORX-UPDATE-002)", nm->str);
|
||||
free(bin);
|
||||
pend_fail = 1;
|
||||
break;
|
||||
}
|
||||
if(pend_n < 2){
|
||||
pend[pend_n].target = target;
|
||||
pend[pend_n].bin = bin;
|
||||
pend[pend_n].len = al;
|
||||
pend_n++;
|
||||
} else {
|
||||
free(bin); /* more binaries than we stage; ignore extras */
|
||||
}
|
||||
}
|
||||
if(!pend_fail && pend_n > 0){
|
||||
for(int pi = 0; pi < pend_n; pi++){
|
||||
if(stage_file(pend[pi].target,
|
||||
(unsigned char*)pend[pi].bin, pend[pi].len) == 0){
|
||||
log_msg("updater: staged %s (%zu bytes)",
|
||||
pend[pi].target, pend[pi].len);
|
||||
updated = 1;
|
||||
}
|
||||
}
|
||||
} else if(pend_fail){
|
||||
log_msg("updater: incomplete set; staged nothing (versions stay matched)");
|
||||
}
|
||||
for(int pi = 0; pi < pend_n; pi++) free(pend[pi].bin);
|
||||
free(sums);
|
||||
free(mbody);
|
||||
}
|
||||
}
|
||||
jl_free(r);
|
||||
return updated ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/* updater.h - self-updater: version compare, staged install. */
|
||||
#ifndef UPDATER_H
|
||||
#define UPDATER_H
|
||||
#include <stddef.h>
|
||||
/* Compare dotted versions ("0.4.0" vs "v0.10.1", leading v ok):
|
||||
* >0 a newer, <0 b newer, 0 equal. Host-testable. */
|
||||
int updater_cmp(const char *a, const char *b);
|
||||
/* Validate a downloaded payload: ELF magic, 64-bit, x86-64, sane size. */
|
||||
int updater_elf_ok(const unsigned char *buf, size_t n);
|
||||
/* Accepts raw ELF (payload .bin) or signed SELF (plugin .prx). */
|
||||
int updater_image_ok(const unsigned char *buf, size_t n);
|
||||
/* Check latest GitHub release; download + atomically stage newer
|
||||
* artifacts the daemon actually runs from. Returns 1 updated,
|
||||
* 0 already current, -1 failed/checked-off. Never fatal. */
|
||||
int updater_check_and_stage(void);
|
||||
#endif
|
||||
@@ -0,0 +1,94 @@
|
||||
/* updater_http.c - pure HTTP response parsing (host-testable + PS4). */
|
||||
#include "updater_http.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <stdio.h>
|
||||
|
||||
char *upd_parse_response(const char *raw, size_t rawlen, size_t cap,
|
||||
int *out_status, size_t *out_len,
|
||||
char *out_location, size_t loc_cap){
|
||||
if(out_status) *out_status = 0;
|
||||
if(out_len) *out_len = 0;
|
||||
if(out_location && loc_cap) out_location[0] = 0;
|
||||
if(!raw || cap < 2) return NULL;
|
||||
/* Split headers/body. */
|
||||
const char *e = strstr(raw, "\r\n\r\n");
|
||||
if(!e || (size_t)(e - raw) > UPD_HDR_MAX) return NULL;
|
||||
size_t hlen = (size_t)(e - raw);
|
||||
const char *body = e + 4;
|
||||
size_t bodylen = (hlen + 4 <= rawlen) ? rawlen - hlen - 4 : 0;
|
||||
int status = 0;
|
||||
if(hlen >= 12 && !memcmp(raw, "HTTP/1.", 7)) status = atoi(raw + 9);
|
||||
if(out_status) *out_status = status;
|
||||
/* Header scan (case-insensitive names, single pass). */
|
||||
long content_len = -1;
|
||||
int chunked = 0;
|
||||
{
|
||||
const char *line = raw;
|
||||
while(line < e){
|
||||
const char *nl = strstr(line, "\r\n");
|
||||
if(!nl || nl > e) nl = e;
|
||||
const char *colon = memchr(line, ':', (size_t)(nl - line));
|
||||
if(colon){
|
||||
size_t nl2 = (size_t)(colon - line);
|
||||
while(nl2 > 0 && (line[nl2-1] == ' ' || line[nl2-1] == '\t')) nl2--;
|
||||
const char *val = colon + 1;
|
||||
while(val < nl && (*val == ' ' || *val == '\t')) val++;
|
||||
size_t vlen = (size_t)(nl - val);
|
||||
if(nl2 == 14 && !strncasecmp(line, "content-length", 14)){
|
||||
char nb[32];
|
||||
size_t cn = vlen < sizeof nb - 1 ? vlen : sizeof nb - 1;
|
||||
memcpy(nb, val, cn); nb[cn] = 0;
|
||||
content_len = atol(nb);
|
||||
} else if(nl2 == 17 && !strncasecmp(line, "transfer-encoding", 17)){
|
||||
if(vlen >= 7 && !strncasecmp(val + vlen - 7, "chunked", 7))
|
||||
chunked = 1;
|
||||
} else if(nl2 == 8 && !strncasecmp(line, "location", 8)){
|
||||
if(out_location && loc_cap > 1){
|
||||
size_t cn = vlen < loc_cap - 1 ? vlen : loc_cap - 1;
|
||||
memcpy(out_location, val, cn);
|
||||
out_location[cn] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
line = (*nl == '\r') ? nl + 2 : nl;
|
||||
if(line >= e) break;
|
||||
}
|
||||
}
|
||||
char *out = NULL;
|
||||
size_t olen = 0;
|
||||
if(chunked){
|
||||
out = (char*)malloc(cap);
|
||||
if(!out) return NULL;
|
||||
size_t pos = 0;
|
||||
while(pos < bodylen){
|
||||
const char *le = strstr(body + pos, "\r\n");
|
||||
if(!le || (size_t)(le - (body + pos)) > 32){ free(out); return NULL; }
|
||||
char nb[33];
|
||||
size_t nlen = (size_t)(le - (body + pos));
|
||||
memcpy(nb, body + pos, nlen); nb[nlen] = 0;
|
||||
long cn = strtol(nb, NULL, 16);
|
||||
if(cn < 0 || cn > (long)(cap - 1)){ free(out); return NULL; }
|
||||
pos += nlen + 2;
|
||||
if(pos > bodylen){ free(out); return NULL; }
|
||||
if(cn == 0) break;
|
||||
if(pos + (size_t)cn + 2 > bodylen){ free(out); return NULL; }
|
||||
if(olen + (size_t)cn > cap - 1){ free(out); return NULL; }
|
||||
memcpy(out + olen, body + pos, (size_t)cn);
|
||||
olen += (size_t)cn;
|
||||
pos += (size_t)cn + 2; /* data + trailing CRLF */
|
||||
}
|
||||
} else {
|
||||
if(content_len >= 0 && (size_t)content_len < bodylen)
|
||||
bodylen = (size_t)content_len;
|
||||
if(bodylen > cap - 1) return NULL;
|
||||
out = (char*)malloc(cap);
|
||||
if(!out) return NULL;
|
||||
memcpy(out, body, bodylen);
|
||||
olen = bodylen;
|
||||
}
|
||||
out[olen] = 0;
|
||||
if(out_len) *out_len = olen;
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/* updater_http.h - pure HTTP response parsing for the updater.
|
||||
*
|
||||
* Host-testable (no PS4 includes): status line, bounded headers,
|
||||
* Content-Length, Transfer-Encoding: chunked decoding, Location capture.
|
||||
* The socket/TLS/redirect-policy half stays in updater.c (PS4-only). */
|
||||
#ifndef ORBISRPC_UPDATER_HTTP_H
|
||||
#define ORBISRPC_UPDATER_HTTP_H
|
||||
#include <stddef.h>
|
||||
|
||||
#define UPD_HDR_MAX 8192
|
||||
|
||||
/* Parse a complete raw HTTP response (rawlen bytes, NUL-terminated at
|
||||
* raw[rawlen]). Returns a heap body (caller frees, NUL-terminated) with
|
||||
* out_len, or NULL on any malformed/truncated/over-cap input.
|
||||
* out_status always set (0 when no valid status line); out_location gets
|
||||
* the redirect target ("" when none). Only the decoded body counts against
|
||||
* cap; headers are separately bounded by UPD_HDR_MAX. */
|
||||
char *upd_parse_response(const char *raw, size_t rawlen, size_t cap,
|
||||
int *out_status, size_t *out_len,
|
||||
char *out_location, size_t loc_cap);
|
||||
#endif
|
||||
@@ -0,0 +1,43 @@
|
||||
/* updater_util.c - host-testable updater logic: version compare, ELF check. */
|
||||
#include "updater.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stddef.h>
|
||||
|
||||
int updater_cmp(const char *a, const char *b){
|
||||
unsigned av[4] = {0,0,0,0}, bv[4] = {0,0,0,0};
|
||||
if(a && (*a=='v'||*a=='V')) a++;
|
||||
if(b && (*b=='v'||*b=='V')) b++;
|
||||
for(int i = 0; i < 4; i++){
|
||||
char *ea = NULL, *eb = NULL;
|
||||
if(a && *a){ av[i] = (unsigned)strtoul(a, &ea, 10); a = ea; if(*a=='.') a++; }
|
||||
if(b && *b){ bv[i] = (unsigned)strtoul(b, &eb, 10); b = eb; if(*b=='.') b++; }
|
||||
}
|
||||
for(int i = 0; i < 4; i++){
|
||||
if(av[i] != bv[i]) return av[i] > bv[i] ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int updater_elf_ok(const unsigned char *buf, size_t n){
|
||||
if(!buf || n < 64) return 0;
|
||||
if(buf[0] != 0x7f || buf[1] != 'E' || buf[2] != 'L' || buf[3] != 'F') return 0;
|
||||
if(buf[4] != 2) return 0; /* 64-bit */
|
||||
if(buf[5] != 1) return 0; /* little-endian */
|
||||
if(buf[18] != 62 || buf[19] != 0) return 0; /* x86-64 */
|
||||
if(n > 8u*1024u*1024u) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* SELF (signed container) check: plugin PRXs ship as SELF, not raw ELF. */
|
||||
static int updater_self_ok(const unsigned char *buf, size_t n){
|
||||
if(!buf || n < 64) return 0;
|
||||
if(buf[0] != 0x4f || buf[1] != 0x15 || buf[2] != 0x3d || buf[3] != 0x1d)
|
||||
return 0;
|
||||
if(n > 8u*1024u*1024u) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int updater_image_ok(const unsigned char *buf, size_t n){
|
||||
return updater_elf_ok(buf, n) || updater_self_ok(buf, n);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/* version.h - single source of truth for the release version.
|
||||
* Bump on every GitHub release; the on-console updater compares this
|
||||
* against the latest release tag. */
|
||||
#ifndef ORBISRPC_VERSION_H
|
||||
#define ORBISRPC_VERSION_H
|
||||
#define ORBISRPC_VERSION "1.0.0"
|
||||
#define ORBISRPC_REPO "SirHumza/orbisRPC"
|
||||
#endif
|
||||
+433
@@ -0,0 +1,433 @@
|
||||
/* ws.c - WebSocket client over SceNet + mbedTLS TLS.
|
||||
* Non-blocking friendly: tls_read() returns 0 when no data is pending, and
|
||||
* 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 "clock.h"
|
||||
#include "tls.h"
|
||||
#include "log.h"
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
/* Payload-SDK build: plain BSD sockets, no Sony modules. getaddrinfo
|
||||
* replaces the kernel resolver; nothing needs initializing. */
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <orbis/Net.h>
|
||||
#include <orbis/Sysmodule.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/socket.h>
|
||||
#endif
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#ifndef SOL_SOCKET
|
||||
#define SOL_SOCKET 0xffff
|
||||
#endif
|
||||
#ifndef SO_NBIO
|
||||
#define SO_NBIO 0x2000
|
||||
#endif
|
||||
|
||||
static int s_net_ready = 0;
|
||||
static int s_net_mem = 0;
|
||||
|
||||
static int net_ensure(void){
|
||||
if(s_net_ready) return 0;
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
/* BSD sockets need no init. */
|
||||
s_net_ready = 1;
|
||||
return 0;
|
||||
#else
|
||||
uint32_t ur = sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_NET);
|
||||
if((int)ur < 0){ log_msg("load NET fail %d", (int)ur); return -1; }
|
||||
if(sceNetInit() < 0){ log_msg("sceNetInit fail"); return -2; }
|
||||
s_net_mem = (int)sceNetPoolCreate("orbisrpcNet", 128*1024, 0);
|
||||
if(s_net_mem < 0){ log_msg("net pool fail %d", s_net_mem); return -3; }
|
||||
/* NOTE: deliberately NO sceNetCtlInit here — this runs inside a game
|
||||
* process, and initializing app-level NetCtl state under a live game is
|
||||
* a crash recipe. The kernel resolver works fine with just the pool. */
|
||||
s_net_ready = 1;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Write exactly n bytes of plaintext through the TLS engine. */
|
||||
static int ws_send_all(ws_t *w, const unsigned char *data, size_t n){
|
||||
return tls_write((tls_ctx_t*)w->tls, data, n) < 0 ? -1 : (int)n;
|
||||
}
|
||||
|
||||
static void next_mask(unsigned char mk[4]){
|
||||
/* RFC 6455 masking key: fresh bytes from /dev/urandom (the same strong
|
||||
* source as TLS). The LCG below is a last-resort fallback for a
|
||||
* console with no urandom at all; masking is obfuscation, not secrecy,
|
||||
* but there is no reason to use a predictable key when OS randomness
|
||||
* exists. */
|
||||
int fd = open("/dev/urandom", O_RDONLY);
|
||||
if(fd >= 0){
|
||||
size_t got = 0;
|
||||
while(got < 4){
|
||||
long r = read(fd, (char *)mk + got, 4 - got);
|
||||
if(r <= 0) break;
|
||||
got += (size_t)r;
|
||||
}
|
||||
close(fd);
|
||||
if(got == 4) return;
|
||||
}
|
||||
static uint32_t mk_seed;
|
||||
if(!mk_seed) mk_seed = (uint32_t)orbis_mono_s() ^ 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));
|
||||
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;
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
/* Resolve via libc (works wherever BSD sockets do). */
|
||||
struct addrinfo hints, *res = NULL;
|
||||
memset(&hints, 0, sizeof hints);
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
{
|
||||
char portbuf[16];
|
||||
snprintf(portbuf, sizeof portbuf, "%d", port);
|
||||
if(getaddrinfo(host, portbuf, &hints, &res) != 0 || !res){
|
||||
log_msg("resolve fail: %s", host);
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
{
|
||||
struct sockaddr_in *a = (struct sockaddr_in *)res->ai_addr;
|
||||
unsigned char *q = (unsigned char *)&a->sin_addr.s_addr;
|
||||
log_msg("dial %s -> %u.%u.%u.%u:%d", host, q[0], q[1], q[2], q[3], port);
|
||||
}
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if(fd < 0){ log_msg("socket fail"); freeaddrinfo(res); goto fail; }
|
||||
w->fd = fd; w->sock = fd;
|
||||
{
|
||||
struct timeval tv = { .tv_sec = 10, .tv_usec = 0 };
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||
}
|
||||
if(connect(fd, res->ai_addr, res->ai_addrlen) < 0){
|
||||
log_msg("connect fail (syscall) to %s:%d", host, port);
|
||||
freeaddrinfo(res);
|
||||
goto fail;
|
||||
}
|
||||
freeaddrinfo(res);
|
||||
{
|
||||
/* Both timeouts stay armed for the socket's whole life (plus
|
||||
* NBIO below): a stuck peer can never wedge the daemon silently —
|
||||
* the failure observed on hardware was a frozen process with zero
|
||||
* output and no timeout to break it. */
|
||||
struct timeval tv0 = { .tv_sec = 15, .tv_usec = 0 };
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv0, sizeof tv0);
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv0, sizeof tv0);
|
||||
}
|
||||
{
|
||||
int fl = fcntl(fd, F_GETFL, 0);
|
||||
if(fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
||||
}
|
||||
w->nb = 1;
|
||||
log_msg("tcp established");
|
||||
#else
|
||||
/* memid = our net pool: passing 0 here fails with EBADF (0x80410109).
|
||||
* If pool creation failed, skip DNS and only allow IP literals. */
|
||||
int32_t rid = -1;
|
||||
OrbisNetInAddr in; memset(&in,0,sizeof in);
|
||||
int resolved = 0;
|
||||
if(s_net_mem < 0){
|
||||
log_msg("net pool invalid; skipping resolver");
|
||||
}else{
|
||||
rid = sceNetResolverCreate("orbisrpcR", s_net_mem, 0);
|
||||
if(rid < 0){
|
||||
log_msg("resolver create fail %d", rid);
|
||||
}else{
|
||||
int32_t rr = sceNetResolverStartNtoa(rid, host, &in, 5000000, 3, 0);
|
||||
if(rr < 0) log_msg("resolver %s err %d", host, rr);
|
||||
sceNetResolverDestroy(rid);
|
||||
resolved = (rr >= 0);
|
||||
}
|
||||
}
|
||||
if(!resolved){
|
||||
struct in_addr ia = { .s_addr = inet_addr(host) };
|
||||
if(ia.s_addr == 0xffffffff){ log_msg("resolve fail: %s", host); goto fail; }
|
||||
in.s_addr = ia.s_addr;
|
||||
}
|
||||
int32_t fd = sceNetSocket("orbisrpcWs", ORBIS_NET_AF_INET, ORBIS_NET_SOCK_STREAM, 0);
|
||||
if(fd < 0){ log_msg("socket fail %d",fd); goto fail; }
|
||||
w->fd = fd; w->sock = fd;
|
||||
/* Pack port + IPv4 into sa_data. The resolver's OrbisNetInAddr already
|
||||
* stores the octets in wire-memory order, so copy the 4 bytes VERBATIM
|
||||
* (any >>24-style arithmetic reverses the IP — v2 dialed mirrored IPs). */
|
||||
unsigned char ipb[4];
|
||||
memcpy(ipb, &in.s_addr, 4);
|
||||
OrbisNetSockaddr sa; memset(&sa,0,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)ipb[0];
|
||||
sa.sa_data[3] = (char)ipb[1];
|
||||
sa.sa_data[4] = (char)ipb[2];
|
||||
sa.sa_data[5] = (char)ipb[3];
|
||||
log_msg("dial %s -> %u.%u.%u.%u:%d", host,
|
||||
(unsigned char)sa.sa_data[2], (unsigned char)sa.sa_data[3],
|
||||
(unsigned char)sa.sa_data[4], (unsigned char)sa.sa_data[5], port);
|
||||
/* Bound the blocking connect: 10s send timeout so a dead route can't
|
||||
* hang the daemon thread forever (plugin_unload joins this thread). */
|
||||
{
|
||||
struct timeval tv = { .tv_sec = 10, .tv_usec = 0 };
|
||||
sceNetSetsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||
}
|
||||
if(sceNetConnect(fd, &sa, sizeof sa) < 0){
|
||||
log_msg("connect fail (syscall) to %s:%d", host, port);
|
||||
goto fail;
|
||||
}
|
||||
{
|
||||
struct timeval tv0 = { .tv_sec = 0, .tv_usec = 0 };
|
||||
sceNetSetsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv0, sizeof tv0);
|
||||
}
|
||||
int on = 1;
|
||||
sceNetSetsockopt(fd, SOL_SOCKET, SO_NBIO, &on, sizeof on);
|
||||
w->nb = 1;
|
||||
log_msg("tcp established");
|
||||
#endif
|
||||
/* TLS handshake over the established connection (mbedTLS, no external
|
||||
* module needed). Socket is NBIO; tls_start pumps it with a deadline. */
|
||||
w->tls = tls_start(fd, host);
|
||||
if(!w->tls){ goto fail; }
|
||||
/* HTTP Upgrade handshake. Desktop-client UA, NO Origin header (native
|
||||
* clients don't send Origin to the gateway). Host has no :port —
|
||||
* browsers never send the default-port suffix and the front side
|
||||
* ignores requests it can't route. */
|
||||
char req[640]; int n=snprintf(req,sizeof req,
|
||||
"GET %s HTTP/1.1\r\nHost: %s\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
|
||||
"User-Agent: 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\r\n"
|
||||
"Sec-WebSocket-Key: %s\r\nSec-WebSocket-Version: 13\r\n\r\n",
|
||||
resource?resource:"/", host, key);
|
||||
if(ws_send_all(w, (const unsigned char*)req, (size_t)n) < 0){ log_msg("hs write fail"); goto fail; }
|
||||
log_msg("hs request sent (%dB)", n);
|
||||
/* 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; int64_t t0=orbis_mono_s();
|
||||
int header_end = -1;
|
||||
extern int daemon_stop_requested(void) __attribute__((weak));
|
||||
while(hlen<(int)sizeof hdr-1){
|
||||
if(daemon_stop_requested && daemon_stop_requested()){ log_msg("ws: hs aborted (stop)"); goto fail; }
|
||||
rd = tls_read(w->tls, hdr+hlen, sizeof hdr-1-(size_t)hlen);
|
||||
if(rd>0){
|
||||
hlen+=rd; hdr[hlen]=0;
|
||||
for(int i=3; i<hlen; i++){
|
||||
if(hdr[i-3]=='\r' && hdr[i-2]=='\n' && hdr[i-1]=='\r' && hdr[i]=='\n'){
|
||||
header_end = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(header_end >= 0) break;
|
||||
continue;
|
||||
}
|
||||
if(rd==0){
|
||||
if(orbis_mono_s()-t0 > 10){ log_msg("hs timeout (got %dB)", hlen); goto fail; }
|
||||
usleep(20000); continue;
|
||||
}
|
||||
log_msg("hs read fail (got %dB)", hlen);
|
||||
goto fail;
|
||||
}
|
||||
if(header_end < 0 || hlen < 12 || strncmp(hdr,"HTTP/1.1 101 ",12)!=0){ log_msg("no 101: %.40s", hdr); goto fail; }
|
||||
w->connected = 1;
|
||||
if(header_end < hlen){
|
||||
const char *body = hdr + header_end;
|
||||
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:
|
||||
tls_free((tls_ctx_t*)w->tls); w->tls=NULL;
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
if(w->fd > 0) close(w->fd);
|
||||
#else
|
||||
if(w->fd > 0) sceNetSocketClose(w->fd);
|
||||
#endif
|
||||
free(w->rbuf); w->rbuf=NULL; w->rcap=0;
|
||||
w->connected=0; w->tls=NULL; w->fd=0; w->sock=0;
|
||||
return -9;
|
||||
}
|
||||
|
||||
int ws_send_text(ws_t *w, const char *msg, size_t len){
|
||||
if(!w || !w->connected || (!msg && len)) return -1;
|
||||
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;
|
||||
/* Small frames (heartbeats, presence) avoid malloc churn in long sessions */
|
||||
unsigned char stack[1024];
|
||||
unsigned char *buf = stack;
|
||||
int use_heap = (f + len > sizeof stack);
|
||||
if(use_heap){
|
||||
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);
|
||||
if(use_heap) free(buf);
|
||||
return r<0?-1:r;
|
||||
}
|
||||
|
||||
/* 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 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);
|
||||
}
|
||||
|
||||
/* Answer a server PING with an (empty) PONG so the gateway keeps us. */
|
||||
int ws_pong(ws_t *w){
|
||||
return ws_send_control(w, 0xA);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
size_t plen = b[1]&0x7f, hdr = 2;
|
||||
if(plen==126){
|
||||
if(avail < 4) return 0;
|
||||
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;
|
||||
}
|
||||
*hdr_out=hdr; *plen_out=(uint64_t)plen;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int valid_frame_header(const unsigned char *b, uint64_t plen){
|
||||
int fin = (b[0] & 0x80) != 0;
|
||||
int rsv = b[0] & 0x70;
|
||||
int op = b[0] & 0x0f;
|
||||
int masked = (b[1] & 0x80) != 0;
|
||||
if (rsv || masked) return 0;
|
||||
if (op >= 3 && op <= 7) return 0; /* reserved opcodes */
|
||||
if (op >= 8 && (!fin || plen > 125)) return 0; /* control: final, <=125 */
|
||||
if ((b[1] & 0x7f) == 127 && (b[2] & 0x80)) return 0; /* absurd 64-bit length */
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ws_recv_frame(ws_t *w, char *buf, size_t cap, int *opcode_out, int *fin_out){
|
||||
if(!w || !w->connected || !buf || cap==0) return -1;
|
||||
for(;;){
|
||||
/* 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;
|
||||
}
|
||||
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;
|
||||
if(!valid_frame_header(b, plen)){
|
||||
log_msg("ws: protocol violation op=%d fin=%d plen=%llu b1=0x%02x b2=0x%02x",
|
||||
b[0]&0x0f, (b[0]&0x80)!=0, (unsigned long long)plen, b[0], b[1]);
|
||||
return -2;
|
||||
}
|
||||
/* absurd length: skip BEFORE arithmetic (hdr+plen could
|
||||
* overflow uint64 and smuggle a tiny "total" past the cap) */
|
||||
if(plen > WS_RBUF_MAX){
|
||||
log_msg("ws: oversized frame op=%d fin=%d plen=%llu (cap %u)",
|
||||
b[0]&0x0f, (b[0]&0x80)!=0, (unsigned long long)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 */
|
||||
log_msg("ws: buffer full with no frame; dropping %zuB and reconnecting", w->rlen);
|
||||
w->rpos=0; w->rlen=0; return -2;
|
||||
}
|
||||
}
|
||||
int rd = tls_read((tls_ctx_t*)w->tls, (char*)w->rbuf+w->rlen, w->rcap-w->rlen);
|
||||
if(rd > 0){ w->rlen += (size_t)rd; continue; }
|
||||
if(rd == 0) return 0; /* no data yet */
|
||||
return -1; /* closed / error */
|
||||
}
|
||||
}
|
||||
|
||||
int ws_close(ws_t *w){
|
||||
if(!w) return -1;
|
||||
if(!w->connected){
|
||||
free(w->rbuf); w->rbuf=NULL; w->rcap=0; w->rlen=0; w->rpos=0;
|
||||
return 0;
|
||||
}
|
||||
ws_send_control(w, 0x8); /* best-effort masked CLOSE */
|
||||
tls_free((tls_ctx_t*)w->tls);
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
close(w->fd);
|
||||
#else
|
||||
sceNetSocketClose(w->fd);
|
||||
#endif
|
||||
free(w->rbuf); w->rbuf=NULL; w->rcap=0;
|
||||
w->connected=0; w->sock=0; w->tls=NULL; w->fd=0; w->rlen=0; w->rpos=0;
|
||||
w->skip_left=0;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/* ws.h - minimal Discord gateway WebSocket (text frames + heartbeat).
|
||||
* Non-blocking: recv never blocks the caller; returns 0 when no complete
|
||||
* frame is available yet. */
|
||||
#ifndef WS_H
|
||||
#define WS_H
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#define WS_RBUF_MIN 65536 /* initial raw socket buffer (READY is big) */
|
||||
#define WS_RBUF_MAX (8*1024*1024) /* grow limit; bigger frames are skipped */
|
||||
|
||||
typedef struct {
|
||||
int32_t sock; int32_t connected; int32_t fd;
|
||||
int nb; /* underlying socket non-blocking flag */
|
||||
void *tls; /* tls_ctx_t* (opaque: mbedTLS session) */
|
||||
unsigned char *rbuf; /* raw bytes from TLS layer (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);
|
||||
/* 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_pong(ws_t *w); /* answer a server PING (call when recv gives opcode 9) */
|
||||
int ws_close(ws_t *w);
|
||||
#endif
|
||||
@@ -0,0 +1,88 @@
|
||||
# orbisRPC GoldHEN plugin build (plugin/).
|
||||
# Builds plugin.prx — a GoldHEN plugin that runs the orbisRPC daemon inside the
|
||||
# game process, so presence auto-starts at every boot with no PC involvement.
|
||||
|
||||
DEBUG_FLAGS = -D__FINAL__=1
|
||||
TYPE = final
|
||||
|
||||
OUTPUT_PRX := orbisrpc_plugin
|
||||
BUILD_DIR := build
|
||||
TARGET := $(BUILD_DIR)/$(OUTPUT_PRX).prx
|
||||
TARGET_ELF := $(BUILD_DIR)/$(OUTPUT_PRX).elf
|
||||
|
||||
# Root vars
|
||||
TOOLCHAIN := $(or $(OO_PS4_TOOLCHAIN),/Users/mac/PS4Toolchain/OpenOrbis/PS4Toolchain)
|
||||
GH_SDK := $(or $(GOLDHEN_SDK),/Users/mac/PS4Toolchain/GoldHEN_Plugins_SDK)
|
||||
ROOT := ..
|
||||
SRCDIR := .
|
||||
ORBISRPC := $(ROOT)/orbisrpc
|
||||
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)/sfo.c $(ORBISRPC)/tmdb_crypto.c $(ORBISRPC)/tmdb.c \
|
||||
$(ORBISRPC)/updater.c $(ORBISRPC)/updater_http.c $(ORBISRPC)/updater_util.c \
|
||||
$(ORBISRPC)/tls.c $(ORBISRPC)/ws.c $(ORBISRPC)/discord.c $(ORBISRPC)/detect.c \
|
||||
$(ORBISRPC)/compat.c $(ORBISRPC)/lock.c $(ORBISRPC)/timesync.c \
|
||||
$(ORBISRPC)/art.c $(ORBISRPC)/health.c $(ORBISRPC)/manifest.c \
|
||||
$(ORBISRPC)/daemon.c
|
||||
PLUGIN_SRC := $(SRCDIR)/plugin.c
|
||||
CFILES := $(PLUGIN_SRC) $(DAEMON_SRC)
|
||||
OBJS := $(patsubst %.c,$(INTDIR)/%.o,$(notdir $(CFILES)))
|
||||
MBED_SRC := $(shell find $(ROOT)/third_party/mbedtls/library -name '*.c' ! -name 'net_sockets.c' ! -name 'timing.c' ! -name 'entropy_poll.c' | sort)
|
||||
MBED_OBJS := $(patsubst $(ROOT)/third_party/mbedtls/library/%.c,$(INTDIR)/mbed/%.o,$(MBED_SRC))
|
||||
|
||||
LIBS := -lSceLibcInternal -lSceNet -lSceNetCtl \
|
||||
-lSceSysmodule -lSceUserService -lSceAppInstUtil -lSceAppContent \
|
||||
-lGoldHEN_Hook -lkernel
|
||||
|
||||
EXTRAFLAGS := $(DEBUG_FLAGS) -std=gnu11 -Wall -Wno-unused \
|
||||
-Wno-int-conversion -Wno-incompatible-pointer-types \
|
||||
-Wno-address-of-packed-member \
|
||||
-DMBEDTLS_NO_PLATFORM_ENTROPY
|
||||
|
||||
CFLAGS := --target=x86_64-pc-freebsd12-elf -fPIC -funwind-tables -c $(EXTRAFLAGS) \
|
||||
-isysroot $(TOOLCHAIN) -isystem $(TOOLCHAIN)/include \
|
||||
-I$(GH_SDK)/include -I$(GH_SDK) -I$(ORBISRPC) -I$(SRCDIR) \
|
||||
-I$(ROOT)/third_party/mbedtls/include
|
||||
LDFLAGS := -m elf_x86_64 -pie --script $(TOOLCHAIN)/link.x -e _init --eh-frame-hdr \
|
||||
-L$(TOOLCHAIN)/lib -L$(GH_SDK) $(LIBS)
|
||||
|
||||
UNAME_S := $(shell uname -s)
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
CC := $(or $(LLVM_CC),/usr/local/opt/llvm/bin/clang)
|
||||
LD := $(or $(LLD),/Users/mac/lldbuild/build/bin/ld.lld)
|
||||
CDIR := macos
|
||||
FSUFFIX := -macos
|
||||
else
|
||||
CC := clang
|
||||
LD := ld.lld
|
||||
CDIR := linux
|
||||
FSUFFIX :=
|
||||
endif
|
||||
|
||||
_unused := $(shell mkdir -p $(INTDIR))
|
||||
|
||||
$(INTDIR)/%.o: $(ORBISRPC)/%.c
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
$(INTDIR)/plugin.o: $(SRCDIR)/plugin.c
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
$(INTDIR)/mbed/%.o: $(ROOT)/third_party/mbedtls/library/%.c
|
||||
@mkdir -p $(dir $@)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
$(TARGET): $(OBJS) $(MBED_OBJS)
|
||||
$(LD) $(GH_SDK)/build/crtprx.o $(OBJS) $(MBED_OBJS) -o $(TARGET_ELF) $(LDFLAGS)
|
||||
OO_PS4_TOOLCHAIN=$(TOOLCHAIN) $(TOOLCHAIN)/bin/$(CDIR)/create-fself$(FSUFFIX) -in=$(TARGET_ELF) -out=$(TARGET_ELF).oelf \
|
||||
--lib=$(TARGET) --paid 0x3800000000000011
|
||||
|
||||
.PHONY: clean
|
||||
.DEFAULT_GOAL := all
|
||||
|
||||
all: $(TARGET)
|
||||
@echo "plugin -> $(TARGET)"
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR)
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/* plugin.c - orbisRPC as a GoldHEN plugin (.prx loaded into the game process).
|
||||
*
|
||||
* GoldHEN loads plugins per game title via /data/GoldHEN/plugins.ini. A
|
||||
* plugin runs inside the game's process, so:
|
||||
* - we KNOW which game is running: sys_sdk_proc_info() -> procInfo.titleid
|
||||
* - the game has full network access (this is the whole point)
|
||||
* - it auto-starts at every boot/jailbreak with zero PC involvement
|
||||
*
|
||||
* plugin_load() resolves the titleid to a display name and starts the shared
|
||||
* daemon thread (daemon.c) which handles token refresh, the Discord gateway
|
||||
* and presence. plugin_unload() stops it cleanly.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <orbis/libkernel.h>
|
||||
#include "Common.h"
|
||||
#include "daemon.h"
|
||||
#include "detect.h"
|
||||
|
||||
/* Only post presence for real game titles (CUSA/PPSA/etc). System apps
|
||||
* (NPXS..., shell) launching would otherwise post bogus "playing" state. */
|
||||
static int is_game_titleid(const char *t){
|
||||
static const char *pfx[] = { "CUSA", "PPSA", "PCSE", "PCJS", "EPSA", "PCSB", NULL };
|
||||
for(int i=0; pfx[i]; i++) if(strncmp(t, pfx[i], 4)==0) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#define attr_public __attribute__((visibility("default")))
|
||||
#define attr_module_hidden __attribute__((weak)) __attribute__((visibility("hidden")))
|
||||
|
||||
attr_public const char *g_pluginName = "orbisrpc";
|
||||
attr_public const char *g_pluginDesc = "Discord Rich Presence for PS4";
|
||||
attr_public const char *g_pluginAuth = "orbisRPC";
|
||||
attr_public uint32_t g_pluginVersion = 0x00000100; /* 1.00 */
|
||||
|
||||
struct proc_info procInfo;
|
||||
|
||||
static OrbisPthread s_daemon_thread;
|
||||
static int s_daemon_started = 0;
|
||||
|
||||
static void *daemon_thread(void *args){
|
||||
/* args = display name of the game we are loaded into */
|
||||
char name[128];
|
||||
strncpy(name, (const char *)args, sizeof name - 1);
|
||||
name[sizeof name - 1] = 0;
|
||||
FILE *m = fopen("/data/orbisRPC/plugin_boot.log", "a");
|
||||
if(m){ fprintf(m, "daemon thread up game=%s\n", name); fclose(m); }
|
||||
daemon_clear_stop();
|
||||
daemon_run(name);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int32_t attr_public plugin_load(int32_t argc, const char *argv[]){
|
||||
(void)argc; (void)argv;
|
||||
klog("[orbisrpc] <%s\\Ver.0x%08x> %s\n", g_pluginName, g_pluginVersion, __func__);
|
||||
klog("[orbisrpc] Plugin Author(s): %s\n", g_pluginAuth);
|
||||
{ FILE *m = fopen("/data/orbisRPC/plugin_boot.log", "a");
|
||||
if(m){ fprintf(m, "plugin_load enter\n"); fclose(m); } }
|
||||
|
||||
if(sys_sdk_proc_info(&procInfo) != 0){
|
||||
klog("[orbisrpc] sys_sdk_proc_info failed; not starting\n");
|
||||
return 0;
|
||||
}
|
||||
/* Validate SDK struct: must be NUL-terminated ASCII, else version mismatch */
|
||||
procInfo.titleid[sizeof procInfo.titleid - 1] = 0;
|
||||
procInfo.name[sizeof procInfo.name - 1] = 0;
|
||||
size_t tlen = strnlen(procInfo.titleid, sizeof procInfo.titleid);
|
||||
if(tlen != 9 || !is_game_titleid(procInfo.titleid)){
|
||||
klog("[orbisrpc] not a game title (system app?); not starting daemon\n");
|
||||
return 0;
|
||||
}
|
||||
klog("[orbisrpc] loaded into process: pid=%d name=%s titleid=%s\n",
|
||||
procInfo.pid, procInfo.name, procInfo.titleid);
|
||||
|
||||
/* Resolve a display name for the title we are loaded into. */
|
||||
static char game_name[128];
|
||||
detect_name_for_title(procInfo.titleid, game_name, sizeof game_name);
|
||||
klog("[orbisrpc] game: %s\n", game_name);
|
||||
|
||||
/* explicit fat stack: TLS handshakes + JSON buffers overflow the
|
||||
* small default plugin-thread stacks. 256KB is mandatory — falling
|
||||
* back to 64KB crashes inside games, so refuse to start instead. */
|
||||
OrbisPthreadAttr attr;
|
||||
memset(&attr, 0, sizeof attr);
|
||||
if(scePthreadAttrInit(&attr) != 0 ||
|
||||
scePthreadAttrSetstacksize(&attr, 256*1024) != 0){
|
||||
klog("[orbisrpc] attr/stack setup failed; NOT starting (no fallback)\n");
|
||||
return 0;
|
||||
}
|
||||
if(scePthreadCreate(&s_daemon_thread, &attr, daemon_thread, game_name, "orbisrpc_daemon") == 0)
|
||||
s_daemon_started = 1;
|
||||
else
|
||||
klog("[orbisrpc] daemon thread creation failed\\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t attr_public plugin_unload(int32_t argc, const char *argv[]){
|
||||
(void)argc; (void)argv;
|
||||
klog("[orbisrpc] <%s\\Ver.0x%08x> %s\n", g_pluginName, g_pluginVersion, __func__);
|
||||
daemon_request_stop();
|
||||
if(s_daemon_started){
|
||||
int rc = scePthreadJoin(s_daemon_thread, NULL);
|
||||
klog("[orbisrpc] daemon thread joined rc=%d\\n", rc);
|
||||
s_daemon_started = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t attr_module_hidden module_start(int64_t argc, const void *args){
|
||||
(void)argc; (void)args;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t attr_module_hidden module_stop(int64_t argc, const void *args){
|
||||
(void)argc; (void)args;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
; GoldHEN plugins.ini — GTA V mod menu + orbisRPC Discord presence
|
||||
;
|
||||
; [default] loads plugins for ANY game title, so orbisRPC auto-starts
|
||||
; whenever a game launches (no PC needed).
|
||||
;
|
||||
[default]
|
||||
/data/GoldHEN/plugins/orbisrpc_plugin.prx=true
|
||||
|
||||
; Lotus SPRX (primary mod menu — requires GTA V 1.48 update)
|
||||
; Press R1 + Square in-game to open
|
||||
;
|
||||
[CUSA00411]
|
||||
/data/GoldHEN/plugins/gtaPayload_148.sprx=true
|
||||
|
||||
[CUSA00419]
|
||||
/data/GoldHEN/plugins/gtaPayload_148.sprx=true
|
||||
|
||||
; Scorpion Menu v1.2B (alternate mod menu — backup option)
|
||||
; Uncomment these lines if you prefer Scorpion over Lotus:
|
||||
;
|
||||
;[CUSA00411]
|
||||
;/data/GoldHEN/plugins/ScorpionMenu-v1.2b-148.prx=true
|
||||
;
|
||||
;[CUSA00419]
|
||||
;/data/GoldHEN/plugins/ScorpionMenu-v1.2b-148.prx=true
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
# build.sh - orbisRPC: compile payload ELF + fself for PS4.
|
||||
# Usage: ./scripts/build.sh [elf|fself|all] default: all
|
||||
# Zero-guess: validates toolchain up front with actionable errors.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
SDK="${OO_PS4_TOOLCHAIN:-/Users/mac/PS4Toolchain/OpenOrbis/PS4Toolchain}"
|
||||
# Auto-detect Homebrew LLVM on macOS if LLVM_HOME not set
|
||||
if [ -z "${LLVM_HOME:-}" ]; then
|
||||
for cand in /opt/homebrew/opt/llvm /usr/local/opt/llvm /usr/local/llvm; do
|
||||
if [ -x "$cand/bin/clang" ]; then LLVM_HOME="$cand"; break; fi
|
||||
done
|
||||
fi
|
||||
LLVM_BIN="${LLVM_HOME:-/usr/local/opt/llvm}/bin"
|
||||
# Auto-detect lld
|
||||
if [ -z "${LLD:-}" ]; then
|
||||
if [ -x "/Users/mac/lldbuild/build/bin/ld.lld" ]; then LLD="/Users/mac/lldbuild/build/bin/ld.lld"
|
||||
elif command -v ld.lld >/dev/null 2>&1; then LLD="$(command -v ld.lld)"
|
||||
else LLD="$LLVM_BIN/ld.lld"
|
||||
fi
|
||||
fi
|
||||
export PATH="$LLVM_BIN:$SDK/bin/macos:$PATH"
|
||||
CC="$LLVM_BIN/clang"
|
||||
LD="$LLD"
|
||||
|
||||
fail() { echo "BUILD FAIL: $1" >&2; exit 1; }
|
||||
[ -x "$CC" ] || fail "clang not found at $CC. Install: brew install llvm OR LLVM_HOME=/path ./scripts/build.sh"
|
||||
[ -x "$LD" ] || fail "ld.lld not found at $LD. Set LLD=/path/to/ld.lld"
|
||||
[ -d "$SDK" ] || fail "OpenOrbis SDK not found at $SDK. Set OO_PS4_TOOLCHAIN=~/PS4Toolchain/OpenOrbis/PS4Toolchain"
|
||||
[ -f "$SDK/lib/crt1.o" ] || fail "missing $SDK/lib/crt1.o — corrupt toolchain download, re-extract v0.5.4"
|
||||
[ -f "$SDK/link.x" ] || fail "missing $SDK/link.x — corrupt toolchain download"
|
||||
[ -f "$SDK/include/orbis/Net.h" ] || fail "SDK headers missing orkNet.h — wrong SDK version?"
|
||||
[ -d "third_party/mbedtls/include" ] || fail "third_party/mbedtls missing — git submodule update --init?"
|
||||
[ -f "third_party/mbedtls/include/mbedtls/ssl.h" ] || fail "mbedtls headers missing"
|
||||
command -v "$SDK/bin/macos/create-fself-macos" >/dev/null 2>&1 || [ -x "$SDK/bin/macos/create-fself-macos" ] || fail "create-fself-macos missing in SDK/bin/macos"
|
||||
|
||||
TARGET="x86_64-pc-freebsd12-elf"
|
||||
CFLAGS="--target=$TARGET -fPIC -std=gnu11 -Wall -Wno-unused \
|
||||
-Wno-int-conversion -Wno-incompatible-pointer-types \
|
||||
-DMBEDTLS_NO_PLATFORM_ENTROPY \
|
||||
-isystem $SDK/include -Ithird_party/mbedtls/include"
|
||||
LIBS="-lc -lkernel -lSceNet -lSceNetCtl -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"
|
||||
export OO_PS4_TOOLCHAIN="$SDK"
|
||||
OUT="$ROOT/build"; mkdir -p "$OUT"
|
||||
echo "=== compiling (CC=$CC LD=$LD SDK=$SDK) ==="
|
||||
for f in log cfg jsonlite b64 sfo tmdb_crypto tmdb updater updater_http updater_util tls ws detect discord daemon compat lock timesync art health manifest main; do
|
||||
"$CC" $CFLAGS -c -o "$OUT/$f.o" "orbisrpc/$f.c" || fail "compile $f"
|
||||
done
|
||||
echo "=== mbedtls (skip net_sockets/timing: POSIX-only) ==="
|
||||
for f in $(find third_party/mbedtls/library -name '*.c' ! -name 'net_sockets.c' ! -name 'timing.c' ! -name 'entropy_poll.c' | sort); do
|
||||
o="$OUT/mbed_$(echo "$f" | sed 's|third_party/mbedtls/library/||; s|\.c$||').o"
|
||||
"$CC" $CFLAGS -c -o "$o" "$f" || fail "mbedtls $f"
|
||||
done
|
||||
echo "=== linking ($LD) ==="
|
||||
"$LD" $OUT/*.o -o "$OUT/orbisrpc.elf" $LDFLAGS || fail "link — see duplicate-symbol or missing-lib errors above"
|
||||
echo "ELF -> $OUT/orbisrpc.elf ($(stat -f%z "$OUT/orbisrpc.elf" 2>/dev/null || stat -c%s "$OUT/orbisrpc.elf") bytes)"
|
||||
MODE="${1:-all}"
|
||||
if [ "$MODE" = "fself" ] || [ "$MODE" = "all" ]; then
|
||||
"$SDK/bin/macos/create-fself-macos" -in="$OUT/orbisrpc.elf" \
|
||||
-out="$OUT/orbisrpc.fself" --eboot "$OUT/orbisrpc-eboot.bin" --paid 0x3800000000000011 || fail "fself creation"
|
||||
echo "FSELF -> $OUT/orbisrpc.fself"
|
||||
echo " (GoldHEN payload to deploy is $OUT/orbisrpc.elf -> /data/GoldHEN/payloads/orbisrpc.bin)"
|
||||
fi
|
||||
echo "done."
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
# build_evict.sh - evict.elf via ps4-payload-sdk (daemon-world linkage only).
|
||||
# Usage: PS4_PAYLOAD_SDK=/path ./scripts/build_evict.sh
|
||||
set -e
|
||||
SDK="${PS4_PAYLOAD_SDK:-$HOME/ps4-payload-sdk/ps4-payload-sdk}"
|
||||
CC="$SDK/bin/orbis-clang"
|
||||
export PS4_PAYLOAD_SDK="$SDK"
|
||||
export PATH="$HOME/llvmshim:$PATH"
|
||||
OUT="build-sdk"
|
||||
mkdir -p "$OUT"
|
||||
echo "=== evict (SDK) ==="
|
||||
"$CC" -O2 -Wall -DORBISRPC_SDK_PAYLOAD -o "$OUT/evict.elf" tools/evict.c || { echo "FAIL: evict"; exit 1; }
|
||||
ls -la "$OUT/evict.elf"
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/bin/sh
|
||||
# build_sdk.sh - OrbisRPC daemon payload via ps4-payload-sdk (daemon-world
|
||||
# linkage: no libkernel.so, BSD sockets, SDK libc). Test vehicle: elfldr.
|
||||
# Usage: PS4_PAYLOAD_SDK=/path ./scripts/build_sdk.sh
|
||||
set -e
|
||||
SDK="${PS4_PAYLOAD_SDK:-$HOME/ps4-payload-sdk/ps4-payload-sdk}"
|
||||
[ -x "$SDK/toolchain/../../ps4-payload-sdk/bin/orbis-clang" ] || {
|
||||
# resolve relative to SDK root regardless of layout
|
||||
true
|
||||
}
|
||||
CC="$SDK/bin/orbis-clang"
|
||||
export PS4_PAYLOAD_SDK="$SDK"
|
||||
export PATH="$HOME/llvmshim:$PATH"
|
||||
OUT="build-sdk"
|
||||
mkdir -p "$OUT"
|
||||
CFLAGS="-O2 -Wall -DORBISRPC_SDK_PAYLOAD -Iorbisrpc -Ithird_party/mbedtls/include -Ithird_party/sqlite/sqlite-amalgamation-3510100"
|
||||
SQLITE_DIR="third_party/sqlite/sqlite-amalgamation-3510100"
|
||||
echo "=== daemon sources (SDK) ==="
|
||||
for f in log cfg jsonlite b64 sfo tmdb_crypto tmdb updater updater_http updater_util tls ws detect discord daemon compat lock timesync art health manifest appdb main; do
|
||||
# clock has no .c (header-only helper lives in compat.c); skip if missing
|
||||
[ -f "orbisrpc/$f.c" ] || continue
|
||||
"$CC" $CFLAGS -c -o "$OUT/$f.o" "orbisrpc/$f.c" || { echo "FAIL: $f"; exit 1; }
|
||||
done
|
||||
echo "=== sqlite (SDK, readonly, no threads/extensions) ==="
|
||||
"$CC" $CFLAGS -Os -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_OMIT_DEPRECATED -c -o "$OUT/sqlite3.o" "$SQLITE_DIR/sqlite3.c" || { echo "FAIL: sqlite"; exit 1; }
|
||||
echo "=== mbedtls (SDK, skip net_sockets/timing/entropy_poll) ==="
|
||||
for f in $(find third_party/mbedtls/library -name '*.c' ! -name 'net_sockets.c' ! -name 'timing.c' ! -name 'entropy_poll.c' | sort); do
|
||||
o="$OUT/mbed_$(echo "$f" | sed 's|third_party/mbedtls/library/||; s|\.c$||').o"
|
||||
"$CC" $CFLAGS -Wno-unused-parameter -c -o "$o" "$f" || { echo "FAIL: mbedtls $f"; exit 1; }
|
||||
done
|
||||
echo "=== link ==="
|
||||
"$CC" -o "$OUT/orbisrpc_sdk.elf" "$OUT"/*.o || { echo "FAIL: link"; exit 1; }
|
||||
ls -la "$OUT/orbisrpc_sdk.elf"
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""make_ca_bundle.py - curate a minimal CA bundle for the PS4 TLS client.
|
||||
|
||||
Extracts the roots needed for GitHub (Sectigo/DigiCert), Discord
|
||||
(Google Trust Services), and Sony TMDB (DigiCert/Amazon/ISRG) from the
|
||||
local OpenSSL bundle, writes orbisrpc/ca_bundle_pem.h as a C string.
|
||||
|
||||
Usage: python3 scripts/make_ca_bundle.py [--system PATH] [--out PATH]
|
||||
Re-run when a handshake fails with ORX-TLS-003 due to root rotation.
|
||||
"""
|
||||
import argparse, os, re, subprocess, sys, tempfile
|
||||
|
||||
WANT = [
|
||||
"Sectigo Public Server Authentication Root E46",
|
||||
"Sectigo Public Server Authentication Root R46",
|
||||
"USERTrust ECC Certification Authority",
|
||||
"USERTrust RSA Certification Authority",
|
||||
"DigiCert Global Root CA",
|
||||
"DigiCert Global Root G2",
|
||||
"DigiCert Global Root G3",
|
||||
"DigiCert High Assurance EV Root CA",
|
||||
"DigiCert Trusted Root G4",
|
||||
"GTS Root R1",
|
||||
"GTS Root R2",
|
||||
"GTS Root R3",
|
||||
"GTS Root R4",
|
||||
"ISRG Root X1",
|
||||
"Amazon Root CA 1",
|
||||
"Baltimore CyberTrust Root",
|
||||
"Starfield Services Root Certificate Authority - G2",
|
||||
"GlobalSign Root CA",
|
||||
]
|
||||
|
||||
|
||||
def split_pem(path):
|
||||
data = open(path).read()
|
||||
return re.findall(r"-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----", data, re.S)
|
||||
|
||||
|
||||
def subject_of(pem):
|
||||
with tempfile.NamedTemporaryFile("w", delete=False, suffix=".pem") as f:
|
||||
f.write(pem)
|
||||
fn = f.name
|
||||
try:
|
||||
out = subprocess.run(["openssl", "x509", "-noout", "-subject", "-in", fn],
|
||||
capture_output=True, text=True).stdout.strip()
|
||||
finally:
|
||||
os.unlink(fn)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--system", default="/usr/local/etc/openssl@3/cert.pem")
|
||||
ap.add_argument("--out", default="orbisrpc/ca_bundle_pem.h")
|
||||
a = ap.parse_args()
|
||||
if not os.path.isfile(a.system):
|
||||
a.system = "/etc/ssl/cert.pem"
|
||||
blocks = split_pem(a.system)
|
||||
picked = []
|
||||
for b in blocks:
|
||||
subj = subject_of(b)
|
||||
for w in WANT:
|
||||
if w.lower() in subj.lower():
|
||||
picked.append(b)
|
||||
break
|
||||
if len(picked) < 8:
|
||||
print("too few roots matched (%d); refusing" % len(picked), file=sys.stderr)
|
||||
return 1
|
||||
c = "/* ca_bundle_pem.h - curated trust anchors (generated, do not edit).\n"
|
||||
c += " * %d roots for GitHub/Discord/Sony. Regenerate with scripts/make_ca_bundle.py.\n" % len(picked)
|
||||
c += " */\n#ifndef ORBISRPC_CA_BUNDLE_PEM_H\n#define ORBISRPC_CA_BUNDLE_PEM_H\n"
|
||||
c += "static const char ORBISRPC_CA_BUNDLE_PEM[] =\n"
|
||||
bundle = "\n".join(picked) + "\n"
|
||||
for line in bundle.splitlines(True):
|
||||
c += '"%s\\n"\n' % line.rstrip("\n").replace('"', "'")
|
||||
c += ";\n#endif\n"
|
||||
open(a.out, "w").write(c)
|
||||
print("wrote %s (%d roots, %d bytes)" % (a.out, len(picked), len(bundle)))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""make_manifest.py - build + sign the release manifest for the updater.
|
||||
|
||||
Usage:
|
||||
python3 scripts/make_manifest.py --version 1.0.1 \
|
||||
--priv release_priv.pem \
|
||||
--bin build/orbisrpc.elf \
|
||||
--prx plugin/build/orbisrpc_plugin.prx \
|
||||
--pkg build/pkg/OrbisRPC-1.0.1.pkg \
|
||||
--out build/pkg/manifest.json
|
||||
|
||||
Outputs manifest.json + manifest.sig (raw 64-byte r||s, hex file) +
|
||||
appends SHA256SUMS for back-compat. The daemon verifies manifest.sig
|
||||
with orbisrpc/release_pubkey.h and refuses updates on mismatch
|
||||
(ORX-UPDATE-002). Keep release_priv.pem OFFLINE, out of the repo.
|
||||
"""
|
||||
import argparse, hashlib, json, os, sys
|
||||
|
||||
try:
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature, encode_dss_signature
|
||||
HAVE_CRYPTO = True
|
||||
except ImportError:
|
||||
HAVE_CRYPTO = False
|
||||
|
||||
|
||||
def sha256_file(path):
|
||||
h = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(65536), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--version", required=True)
|
||||
ap.add_argument("--channel", default="stable")
|
||||
ap.add_argument("--min-version", default="")
|
||||
ap.add_argument("--platform", default="ps4-goldhen")
|
||||
ap.add_argument("--priv", required=True, help="EC P-256 private key PEM (offline)")
|
||||
ap.add_argument("--bin", default="")
|
||||
ap.add_argument("--prx", default="")
|
||||
ap.add_argument("--pkg", default="")
|
||||
ap.add_argument("--out", required=True)
|
||||
a = ap.parse_args()
|
||||
|
||||
if not HAVE_CRYPTO:
|
||||
print("need python cryptography package: pip install cryptography", file=sys.stderr)
|
||||
return 1
|
||||
assets = []
|
||||
for name, path in (("orbisrpc.bin", a.bin), ("orbisrpc_plugin.prx", a.prx)):
|
||||
if path and os.path.isfile(path):
|
||||
assets.append({"name": name, "sha256": sha256_file(path)})
|
||||
pkg_name = ""
|
||||
if a.pkg and os.path.isfile(a.pkg):
|
||||
pkg_name = os.path.basename(a.pkg)
|
||||
assets.append({"name": pkg_name, "sha256": sha256_file(a.pkg)})
|
||||
if not assets:
|
||||
print("no assets found (bin/prx/pkg missing)", file=sys.stderr)
|
||||
return 1
|
||||
manifest = {
|
||||
"version": a.version,
|
||||
"channel": a.channel,
|
||||
"platform": a.platform,
|
||||
"assets": assets,
|
||||
}
|
||||
if a.min_version:
|
||||
manifest["minimum_version"] = a.min_version
|
||||
body = json.dumps(manifest, sort_keys=True, separators=(",", ":")).encode()
|
||||
priv = serialization.load_pem_private_key(open(a.priv, "rb").read(), password=None)
|
||||
if not isinstance(priv, ec.EllipticCurvePrivateKey):
|
||||
print("private key is not EC", file=sys.stderr)
|
||||
return 1
|
||||
der = priv.sign(body, ec.ECDSA(hashes.SHA256()))
|
||||
r, s = decode_dss_signature(der)
|
||||
raw = r.to_bytes(32, "big") + s.to_bytes(32, "big")
|
||||
with open(a.out, "wb") as f:
|
||||
f.write(body)
|
||||
with open(a.out + ".sig", "wb") as f:
|
||||
f.write(raw)
|
||||
with open(a.out + ".sig.hex", "w") as f:
|
||||
f.write(raw.hex())
|
||||
# back-compat SHA256SUMS alongside
|
||||
sums = os.path.join(os.path.dirname(a.out), "SHA256SUMS")
|
||||
with open(sums, "w") as f:
|
||||
for ent in assets:
|
||||
f.write("%s %s\n" % (ent["sha256"], ent["name"]))
|
||||
f.write("%s %s\n" % (hashlib.sha256(body).hexdigest(), "manifest.json"))
|
||||
print("wrote %s (%d assets) + %s.sig + SHA256SUMS" % (a.out, len(assets), a.out))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
# sync_icons.sh - pull /user/appmeta/<TITLEID>/icon0.png off the PS4 into
|
||||
# config/icons/<lower titleid>.png, ready to host as the art pack.
|
||||
# Usage: ./scripts/sync_icons.sh [PS4_IP] default: 192.168.1.136
|
||||
# Nothing here touches the console beyond FTP reads.
|
||||
set -euo pipefail
|
||||
IP="${1:-192.168.1.136}"
|
||||
OUT="config/icons"
|
||||
mkdir -p "$OUT"
|
||||
python3 - "$IP" "$OUT" <<'EOF'
|
||||
import sys, os
|
||||
from ftplib import FTP
|
||||
ip, out = sys.argv[1], sys.argv[2]
|
||||
f = FTP()
|
||||
f.connect(ip, 2121, timeout=15)
|
||||
f.login()
|
||||
names = f.nlst('/user/appmeta')
|
||||
got, miss = 0, []
|
||||
for t in names:
|
||||
if len(t) != 9:
|
||||
continue
|
||||
dst = os.path.join(out, t.lower() + '.png')
|
||||
if os.path.exists(dst):
|
||||
got += 1
|
||||
continue
|
||||
try:
|
||||
with open(dst, 'wb') as fh:
|
||||
f.retrbinary('RETR /user/appmeta/%s/icon0.png' % t, fh.write)
|
||||
# validate PNG magic
|
||||
with open(dst, 'rb') as fh:
|
||||
if fh.read(8) != b'\x89PNG\r\n\x1a\n':
|
||||
os.remove(dst)
|
||||
miss.append(t)
|
||||
continue
|
||||
got += 1
|
||||
except Exception:
|
||||
miss.append(t)
|
||||
try: os.remove(dst)
|
||||
except OSError: pass
|
||||
print('icons: %d ok, %d missing (%s)' % (got, len(miss), ','.join(miss[:10])))
|
||||
EOF
|
||||
@@ -0,0 +1,79 @@
|
||||
CC ?= cc
|
||||
CFLAGS += -std=c11 -Wall -Wextra -Werror -DHEALTH_TESTABLE -I../orbisrpc -I../third_party/mbedtls/include -I$(SQLITE_DIR)
|
||||
|
||||
TARGET := test_utils
|
||||
OBJDIR := .objs
|
||||
ASAN_OBJDIR := .objs-asan
|
||||
MBEDTLS_DIR := ../third_party/mbedtls/library
|
||||
# Same exclusion set as scripts/build.sh (POSIX-only modules).
|
||||
MBEDTLS_SRCS := $(filter-out $(MBEDTLS_DIR)/net_sockets.c $(MBEDTLS_DIR)/timing.c $(MBEDTLS_DIR)/entropy_poll.c,$(wildcard $(MBEDTLS_DIR)/*.c))
|
||||
|
||||
ORBIS_SRCS := ../orbisrpc/jsonlite.c ../orbisrpc/b64.c ../orbisrpc/sfo.c ../orbisrpc/tmdb_crypto.c ../orbisrpc/updater_util.c ../orbisrpc/art.c ../orbisrpc/log.c ../orbisrpc/health.c ../orbisrpc/manifest.c ../orbisrpc/compat.c ../orbisrpc/cfg.c ../orbisrpc/appdb.c ../orbisrpc/discord.c
|
||||
INST_SRCS := ../installer/icfg.c ../installer/nettest.c ../installer/send.c
|
||||
# SQLite amalgamation: -O0 for host iteration speed (payload uses -O2).
|
||||
SQLITE_DIR := ../third_party/sqlite/sqlite-amalgamation-3510100
|
||||
SQLITE_FLAGS := -O0 -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_OMIT_DEPRECATED -I$(SQLITE_DIR)
|
||||
|
||||
ORBIS_OBJS := $(patsubst ../orbisrpc/%.c,$(OBJDIR)/orbis_%.o,$(ORBIS_SRCS))
|
||||
INST_OBJS := $(patsubst ../installer/%.c,$(OBJDIR)/inst_%.o,$(INST_SRCS))
|
||||
MBEDTLS_OBJS := $(patsubst $(MBEDTLS_DIR)/%.c,$(OBJDIR)/mbed_%.o,$(MBEDTLS_SRCS))
|
||||
TEST_OBJS := $(OBJDIR)/test_utils.o $(ORBIS_OBJS) $(INST_OBJS) $(MBEDTLS_OBJS) $(OBJDIR)/sqlite3.o
|
||||
|
||||
ASAN_FLAGS := -fsanitize=address,undefined -fno-omit-frame-pointer
|
||||
ASAN_ORBIS_OBJS := $(patsubst ../orbisrpc/%.c,$(ASAN_OBJDIR)/orbis_%.o,$(ORBIS_SRCS))
|
||||
ASAN_INST_OBJS := $(patsubst ../installer/%.c,$(ASAN_OBJDIR)/inst_%.o,$(INST_SRCS))
|
||||
ASAN_MBEDTLS_OBJS := $(patsubst $(MBEDTLS_DIR)/%.c,$(ASAN_OBJDIR)/mbed_%.o,$(MBEDTLS_SRCS))
|
||||
ASAN_OBJS := $(ASAN_OBJDIR)/test_utils.o $(ASAN_ORBIS_OBJS) $(ASAN_INST_OBJS) $(ASAN_MBEDTLS_OBJS) $(ASAN_OBJDIR)/sqlite3.o
|
||||
|
||||
.PHONY: all clean test asan
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
test: $(TARGET)
|
||||
./$(TARGET)
|
||||
|
||||
$(TARGET): $(TEST_OBJS)
|
||||
$(CC) $(CFLAGS) -o $@ $(TEST_OBJS) -lm
|
||||
|
||||
asan: $(ASAN_OBJS)
|
||||
$(CC) $(CFLAGS) $(ASAN_FLAGS) -o test_utils_asan $(ASAN_OBJS) -lm
|
||||
./test_utils_asan
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(ASAN_OBJDIR):
|
||||
mkdir -p $(ASAN_OBJDIR)
|
||||
|
||||
$(OBJDIR)/test_utils.o: test_utils.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ test_utils.c
|
||||
|
||||
$(OBJDIR)/orbis_%.o: ../orbisrpc/%.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/inst_%.o: ../installer/%.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -I../installer -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/mbed_%.o: $(MBEDTLS_DIR)/%.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -Wno-unused-parameter -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/sqlite3.o: $(SQLITE_DIR)/sqlite3.c | $(OBJDIR)
|
||||
$(CC) $(SQLITE_FLAGS) -c -o $@ $<
|
||||
|
||||
$(ASAN_OBJDIR)/sqlite3.o: $(SQLITE_DIR)/sqlite3.c | $(ASAN_OBJDIR)
|
||||
$(CC) $(SQLITE_FLAGS) $(ASAN_FLAGS) -c -o $@ $<
|
||||
|
||||
$(ASAN_OBJDIR)/test_utils.o: test_utils.c | $(ASAN_OBJDIR)
|
||||
$(CC) $(CFLAGS) $(ASAN_FLAGS) -c -o $@ test_utils.c
|
||||
|
||||
$(ASAN_OBJDIR)/orbis_%.o: ../orbisrpc/%.c | $(ASAN_OBJDIR)
|
||||
$(CC) $(CFLAGS) $(ASAN_FLAGS) -c -o $@ $<
|
||||
|
||||
$(ASAN_OBJDIR)/inst_%.o: ../installer/%.c | $(ASAN_OBJDIR)
|
||||
$(CC) $(CFLAGS) $(ASAN_FLAGS) -I../installer -c -o $@ $<
|
||||
|
||||
$(ASAN_OBJDIR)/mbed_%.o: $(MBEDTLS_DIR)/%.c | $(ASAN_OBJDIR)
|
||||
$(CC) $(CFLAGS) $(ASAN_FLAGS) -Wno-unused-parameter -c -o $@ $<
|
||||
|
||||
clean:
|
||||
rm -rf $(TARGET) test_utils_asan $(OBJDIR) $(ASAN_OBJDIR) *.o
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""e2e_consumer.py - host verification of the consumer flow for the SDK line.
|
||||
Checks source contracts + runs the compiled unit tests + gates the payload
|
||||
linkage (daemon-world libs only). Exit nonzero on any failure.
|
||||
Honest about what is host-verified (logic/contracts) vs hardware-only
|
||||
(presence display, loaders, Rest Mode)."""
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
fails = []
|
||||
|
||||
|
||||
def check(name, cond, note=""):
|
||||
print(("PASS " if cond else "FAIL ") + name + (" - " + note if note else ""))
|
||||
if not cond:
|
||||
fails.append(name)
|
||||
|
||||
|
||||
def src(p):
|
||||
with open(os.path.join(ROOT, p), encoding="utf-8", errors="replace") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
# 1. unit tests build + pass (covers json, sfo, tmdb, updater, art, health…)
|
||||
r = subprocess.run(["make", "-C", os.path.join(ROOT, "tests"), "clean"],
|
||||
capture_output=True)
|
||||
r = subprocess.run(["make", "-C", os.path.join(ROOT, "tests"), "test"],
|
||||
capture_output=True, text=True)
|
||||
check("host/unit-tests", r.returncode == 0 and "utility tests passed" in r.stdout + r.stderr)
|
||||
# 2. config template ships no real token, sane defaults, art pack default
|
||||
cfg = src("config/config.json")
|
||||
check("install/template-has-no-token", "SET_ME" in cfg)
|
||||
check("install/art-pack-default", "orbisrpc-host" in cfg)
|
||||
# 3. single-instance lock with recycled-PID guard
|
||||
lock = src("orbisrpc/lock.c")
|
||||
check("runtime/single-lock", "lock_acquire" in src("orbisrpc/daemon.c"))
|
||||
check("runtime/lock-name-check", "Payload" in lock)
|
||||
# 4. session: persistence file, resume window, integer ms, since null
|
||||
d = src("orbisrpc/daemon.c")
|
||||
check("session/persisted", "session.json" in d)
|
||||
check("session/resume-window", "SESSION_RESUMED" in d)
|
||||
check("session/ledger", "playtime.log" in d)
|
||||
disc = src("orbisrpc/discord.c")
|
||||
check("discord/ms-integer", "jl_new_int" in disc and "1000LL" in disc)
|
||||
check("discord/since-null", "jl_new_null" in disc)
|
||||
check("discord/media-types", "detect_media_type" in disc)
|
||||
# 5. crash recovery wired
|
||||
check("health/boot-marker", "health_boot_note_crash" in d)
|
||||
check("health/safe-mode-gates-updater", "safe_mode" in d)
|
||||
check("health/rollback-at-boot", "health_verify_or_rollback" in d)
|
||||
# 6. time correction
|
||||
check("clock/sntp", "time_sync" in d and "time_fixed" in d)
|
||||
check("clock/monotonic-deadlines", "orbis_mono_s" in src("orbisrpc/ws.c"))
|
||||
# 7. art: mp: proxy path, no raw URLs, pack default
|
||||
check("art/mp-proxy", "art_resolve_mp" in disc)
|
||||
check("art/no-raw-urls", "mp:" in src("orbisrpc/art.c"))
|
||||
check("art/live-tmdb", "https_get_tmdb" in src("orbisrpc/tmdb.c"))
|
||||
check("art/no-baked-table", "art_table" not in src("orbisrpc/tmdb.c").lower())
|
||||
# 8. home + rest handling
|
||||
check("home/presence", "presence: home" in d)
|
||||
check("shutdown/signals", "daemon_on_signal" in d and "lock_release" in d)
|
||||
check("shutdown/session-persisted", "session.json" in d)
|
||||
check("reconnect/jitter-attempts", "reconnect_delay" in d and "attempt %d" in d)
|
||||
check("reconnect/never-exits", "600" in d and "quiet persistence" in d)
|
||||
check("metrics/hourly", "HEALTH:" in d)
|
||||
check("queue/failed-post-retries", "will retry" in d)
|
||||
check("state/explicit", "pres_set" in d and "STATE: presence" in d)
|
||||
check("art/disk-cache", "artwork_cache.json" in src("orbisrpc/art.c"))
|
||||
check("cfg/schema-save-checked", "cfg_save" in d and "unwritable" in d)
|
||||
# 9. signed updates, no unsigned fallback
|
||||
upd = src("orbisrpc/updater.c")
|
||||
check("update/manifest-required", "manifest" in upd.lower())
|
||||
check("update/unsigned-refused", "refus" in upd.lower())
|
||||
# 10. payload linkage gate (daemon-world libs only)
|
||||
elf = os.path.join(ROOT, "build-sdk", "orbisrpc_sdk.elf")
|
||||
if os.path.exists(elf):
|
||||
readelf = os.environ.get("LLVM_READELF", "/usr/local/opt/llvm/bin/llvm-readelf")
|
||||
try:
|
||||
out = subprocess.run([readelf, "-d", elf], capture_output=True,
|
||||
text=True).stdout
|
||||
except OSError:
|
||||
out = ""
|
||||
needed = re.findall(r"\[(.*?)\]", out)
|
||||
check("linkage/daemon-libs",
|
||||
any("libkernel_web" in n for n in needed) and
|
||||
any("SceLibcInternal" in n for n in needed))
|
||||
check("linkage/no-app-libs",
|
||||
not any(n in ("libkernel.so", "libc.so") for n in needed),
|
||||
",".join(needed))
|
||||
check("detect/unknown-holds", "scan_unknown" in d)
|
||||
check("detect/eboot-fast-switch", "fast-switching" in d)
|
||||
check("detect/atime-identity", "pkg-atime" in src("orbisrpc/detect.c") or "st_atime" in src("orbisrpc/detect.c"))
|
||||
check("detect/no-baked-table", "nametable" not in src("orbisrpc/detect.c").lower())
|
||||
check("session/validates-restore", "failed validation" in d)
|
||||
elf = os.path.join(ROOT, "build-sdk", "orbisrpc_sdk.elf")
|
||||
if not os.path.exists(elf):
|
||||
check("linkage/payload-built", False, "run scripts/build_sdk.sh first")
|
||||
print(f"{len(fails)} failures" if fails else "E2E host simulation: ALL PASS")
|
||||
sys.exit(1 if fails else 0)
|
||||
@@ -0,0 +1,642 @@
|
||||
#include "../orbisrpc/jsonlite.h"
|
||||
#include "../orbisrpc/b64.h"
|
||||
#include "../orbisrpc/sfo.h"
|
||||
#include "../orbisrpc/tmdb_crypto.h"
|
||||
#include "../orbisrpc/updater.h"
|
||||
#include "../orbisrpc/art.h"
|
||||
#include "../orbisrpc/health.h"
|
||||
#include "../orbisrpc/manifest.h"
|
||||
#include "../orbisrpc/cfg.h"
|
||||
#include "../orbisrpc/appdb.h"
|
||||
#include "../orbisrpc/discord.h"
|
||||
#include "../orbisrpc/detect.h"
|
||||
#include "../installer/icfg.h"
|
||||
#include "../installer/nettest.h"
|
||||
#include "sqlite3.h"
|
||||
#include <string.h>
|
||||
|
||||
/* discord.c host stubs: builder tests never touch the wire. */
|
||||
int detect_media_type(const char *t){
|
||||
if(t && !strcmp(t, "CUSA00127")) return 3;
|
||||
return 0;
|
||||
}
|
||||
int ws_connect(ws_t *w, const char *h, int p, const char *r, const char *k){
|
||||
(void)w; (void)h; (void)p; (void)r; (void)k; return -1;
|
||||
}
|
||||
int ws_send_text(ws_t *w, const char *m, size_t n){
|
||||
(void)w; (void)m; (void)n; return -1;
|
||||
}
|
||||
int ws_recv_frame(ws_t *w, char *b, size_t c, int *o, int *f){
|
||||
(void)w; (void)b; (void)c; (void)o; (void)f; return -1;
|
||||
}
|
||||
int ws_pong(ws_t *w){ (void)w; return -1; }
|
||||
int ws_close(ws_t *w){ (void)w; return -1; }
|
||||
#include <mbedtls/ecdsa.h>
|
||||
#include <mbedtls/ecp.h>
|
||||
#include <mbedtls/ctr_drbg.h>
|
||||
#include <mbedtls/entropy.h>
|
||||
#include <mbedtls/sha256.h>
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
/* Portable temp dir (mkdtemp needs feature macros this toolchain lacks). */
|
||||
static int make_tmpdir(char *out, size_t cap){
|
||||
static int seq = 0;
|
||||
snprintf(out, cap, "/tmp/orx_test_%d_%d", (int)getpid(), seq++);
|
||||
if(mkdir(out, 0700) != 0) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void test_json(void) {
|
||||
const char input[] = "{\"name\":\"A\\u00e9\",\"items\":[true,2,null]}";
|
||||
jl_val_t *root = jl_parse(input, sizeof(input) - 1);
|
||||
assert(root && root->type == JL_OBJECT);
|
||||
assert(strcmp(jl_obj_get(root, "name")->str, "A\xc3\xa9") == 0);
|
||||
assert(jl_arr_at(jl_obj_get(root, "items"), 0)->num == 1);
|
||||
assert(jl_arr_at(jl_obj_get(root, "items"), 1)->num == 2);
|
||||
assert(jl_arr_at(jl_obj_get(root, "items"), 2)->type == JL_NULL);
|
||||
jl_free(root);
|
||||
|
||||
assert(jl_parse("{} trailing", 11) == NULL);
|
||||
assert(jl_parse("[1,]", 4) == NULL);
|
||||
assert(jl_parse("\"unterminated", 13) == NULL);
|
||||
}
|
||||
|
||||
static void test_gateway_op_spoof(void) {
|
||||
/* string value containing `"op":` must not spoof the real op */
|
||||
const char input[] = "{\"note\":\"x \\\"op\\\":99 y\",\"op\":10,\"s\":42}";
|
||||
jl_val_t *r = jl_parse(input, sizeof(input)-1);
|
||||
assert(r && r->type == JL_OBJECT);
|
||||
const jl_val_t *op = jl_obj_get(r, "op");
|
||||
const jl_val_t *s = jl_obj_get(r, "s");
|
||||
assert(op && op->type == JL_NUMBER && (int)op->num == 10);
|
||||
assert(s && s->type == JL_NUMBER && (int)s->num == 42);
|
||||
jl_free(r);
|
||||
/* missing op -> NULL, not crash */
|
||||
const char no_op[] = "{\"t\":\"READY\"}";
|
||||
jl_val_t *r2 = jl_parse(no_op, sizeof(no_op)-1);
|
||||
assert(r2 && jl_obj_get(r2, "op") == NULL);
|
||||
jl_free(r2);
|
||||
}
|
||||
|
||||
static void test_json_oom_safe(void) { assert(jl_parse("true", 4) != NULL);
|
||||
assert(jl_parse("false", 5) != NULL);
|
||||
assert(jl_parse("null", 4) != NULL);
|
||||
jl_val_t *n = jl_parse("123.5", 5);
|
||||
assert(n && n->type == JL_NUMBER);
|
||||
jl_free(n);
|
||||
/* incomplete pair must fail cleanly, no leak/crash */
|
||||
assert(jl_parse("{\"a\":", 5) == NULL);
|
||||
assert(jl_parse("{\"a\":1", 6) == NULL);
|
||||
/* oversize input refused before any allocation */
|
||||
assert(jl_parse("[]", 5u*1024u*1024u) == NULL);
|
||||
}
|
||||
static void test_json_hostile(void) {
|
||||
/* 200-deep nesting must be rejected, not stack-smash */
|
||||
char deep[420];
|
||||
memset(deep, '[', 200);
|
||||
memset(deep+200, ']', 200);
|
||||
deep[400] = 0;
|
||||
assert(jl_parse(deep, 400) == NULL);
|
||||
/* 60-deep is fine */
|
||||
char okd[130];
|
||||
memset(okd, '[', 60);
|
||||
memset(okd+60, ']', 60);
|
||||
okd[120] = 0;
|
||||
jl_val_t *r = jl_parse(okd, 120);
|
||||
assert(r && r->type == JL_ARRAY);
|
||||
jl_free(r);
|
||||
/* number running exactly to buffer end (no NUL past it) */
|
||||
char num[16];
|
||||
memcpy(num, "{\"s\":41250}", 11);
|
||||
jl_val_t *r2 = jl_parse(num, 11);
|
||||
assert(r2);
|
||||
assert(jl_obj_get(r2, "s")->num == 41250);
|
||||
jl_free(r2);
|
||||
/* absurd number token fails cleanly */
|
||||
char big[80];
|
||||
memset(big, '9', 70);
|
||||
big[70] = 0;
|
||||
assert(jl_parse(big, 70) == NULL);
|
||||
}
|
||||
static void test_sfo(void) {
|
||||
/* minimal synthetic param.sfo: header + 1 entry (TITLE="Terraria") */
|
||||
unsigned char sfo[128];
|
||||
char out[64];
|
||||
memset(sfo, 0, sizeof sfo);
|
||||
sfo[0]=0x00; sfo[1]='P'; sfo[2]='S'; sfo[3]='F';
|
||||
sfo[4]=0x01; sfo[5]=0x02;
|
||||
sfo[8]=36; sfo[12]=48; sfo[16]=1; /* keys@36 data@48 */
|
||||
sfo[20]=0; sfo[21]=0; sfo[22]=0x04; sfo[23]=0; /* key_off=0 fmt=0x0004 */
|
||||
sfo[24]=9; sfo[28]=16; sfo[32]=0; /* len=9 max=16 data_off=0 */
|
||||
memcpy(sfo+36, "TITLE", 6);
|
||||
memcpy(sfo+48, "Terraria", 9);
|
||||
assert(sfo_title(sfo, 64, out, sizeof out) == 0);
|
||||
assert(strcmp(out, "Terraria") == 0);
|
||||
/* malformed: bad magic, truncated, insane count, OOB offsets */
|
||||
unsigned char bad[64];
|
||||
memset(bad, 0, sizeof bad);
|
||||
assert(sfo_title(bad, sizeof bad, out, sizeof out) != 0);
|
||||
assert(sfo_title(sfo, 10, out, sizeof out) != 0);
|
||||
sfo[16]=200; /* count overflow */
|
||||
assert(sfo_title(sfo, 64, out, sizeof out) != 0);
|
||||
sfo[16]=1; sfo[8]=200; /* key_off OOB */
|
||||
assert(sfo_title(sfo, 64, out, sizeof out) != 0);
|
||||
sfo[8]=36;
|
||||
/* tiny output buffer still safe */
|
||||
assert(sfo_title(sfo, 64, out, 4) == 0);
|
||||
assert(out[3] == 0);
|
||||
/* unterminated key region: must fail, not read OOB */
|
||||
memset(sfo, 0x41, sizeof sfo);
|
||||
sfo[0]=0x00; sfo[1]='P'; sfo[2]='S'; sfo[3]='F';
|
||||
sfo[8]=36; sfo[12]=48; sfo[16]=1;
|
||||
sfo[20]=0; sfo[22]=0x04; sfo[24]=9; sfo[28]=16; sfo[32]=0;
|
||||
assert(sfo_title(sfo, 64, out, sizeof out) != 0);
|
||||
}
|
||||
static void test_tmdb(void) {
|
||||
unsigned char dig[20];
|
||||
char hex[41];
|
||||
int i;
|
||||
/* SHA1("abc") = a9993e364706816aba3e25717850c26c9cd0d4d */
|
||||
tmdb_sha1((const unsigned char *)"abc", 3, dig);
|
||||
for(i=0;i<20;i++) snprintf(hex+2*i, 3, "%02x", dig[i]);
|
||||
assert(strcmp(hex, "a9993e364706816aba3e25717850c26c9cd0d89d") == 0);
|
||||
/* HMAC-SHA1 RFC 2202 case 1 */
|
||||
{
|
||||
unsigned char key[20];
|
||||
memset(key, 0x0b, 20);
|
||||
tmdb_hmac_sha1(key, 20, (const unsigned char *)"Hi There", 8, dig);
|
||||
for(i=0;i<20;i++) snprintf(hex+2*i, 3, "%02x", dig[i]);
|
||||
assert(strcmp(hex, "b617318655057264e28bc0b6fb378c8ef146be00") == 0);
|
||||
}
|
||||
/* URL path must match the hash Sony's live service accepts */
|
||||
{
|
||||
char path[128];
|
||||
assert(tmdb_path("CUSA00740", path, sizeof path) == 0);
|
||||
assert(strcmp(path, "/tmdb2/CUSA00740_00_95C83DE844D155477CB77C577A24D735F2E9AC08/CUSA00740_00.json") == 0);
|
||||
assert(tmdb_path("junk!", path, sizeof path) != 0);
|
||||
assert(tmdb_path("CUSA00740", path, 10) != 0);
|
||||
}
|
||||
/* response parse: name + icon URL */
|
||||
{
|
||||
const char body[] = "{\"names\":[{\"name\":\"Terraria\"}],\"icons\":[{\"icon\":\"http://x/y/icon0.png\",\"type\":\"512x512\"}]}";
|
||||
char name[64], icon[128];
|
||||
assert(tmdb_parse(body, sizeof(body)-1, name, sizeof name, icon, sizeof icon) == 0);
|
||||
assert(strcmp(name, "Terraria") == 0);
|
||||
assert(strcmp(icon, "http://x/y/icon0.png") == 0);
|
||||
/* non-URL icon rejected, name still wins */
|
||||
const char body2[] = "{\"names\":[{\"name\":\"X\"}],\"icons\":[{\"icon\":\"not a url\"}]}";
|
||||
assert(tmdb_parse(body2, sizeof(body2)-1, name, sizeof name, icon, sizeof icon) == 0);
|
||||
assert(icon[0] == 0);
|
||||
/* no names -> fail */
|
||||
assert(tmdb_parse("{\"icons\":[]}", 12, name, sizeof name, icon, sizeof icon) != 0);
|
||||
}
|
||||
}
|
||||
static void test_updater(void) {
|
||||
assert(updater_cmp("0.4.0", "0.4.0") == 0);
|
||||
assert(updater_cmp("v0.4.1", "0.4.0") > 0);
|
||||
assert(updater_cmp("0.4.0", "v0.4.1") < 0);
|
||||
assert(updater_cmp("0.10.0", "0.9.9") > 0);
|
||||
assert(updater_cmp("1.0", "1.0.0") == 0);
|
||||
assert(updater_cmp(NULL, "0.1") < 0);
|
||||
unsigned char elf[64];
|
||||
memset(elf, 0, sizeof elf);
|
||||
assert(updater_elf_ok(elf, sizeof elf) == 0);
|
||||
assert(updater_elf_ok(NULL, 100) == 0);
|
||||
assert(updater_elf_ok(elf, 10) == 0);
|
||||
elf[0]=0x7f; elf[1]='E'; elf[2]='L'; elf[3]='F';
|
||||
elf[4]=2; elf[5]=1; elf[18]=62; elf[19]=0;
|
||||
assert(updater_elf_ok(elf, sizeof elf) == 1);
|
||||
elf[18]=99;
|
||||
assert(updater_elf_ok(elf, sizeof elf) == 0);
|
||||
}
|
||||
static void test_art_parse(void) {
|
||||
/* Real external-assets response shape (verified against live API). */
|
||||
const char *body = "[{\"url\":\"https://example.com/a.png\","
|
||||
"\"external_asset_path\":\"external/ABC/https/example.com/a.png\"},"
|
||||
"{\"url\":\"https://example.com/b.png\","
|
||||
"\"external_asset_path\":\"external/DEF/https/example.com/b.png\"}]";
|
||||
char out[128] = {0};
|
||||
assert(art_parse_mp(body, strlen(body),
|
||||
"https://example.com/b.png", out, sizeof out) == 1);
|
||||
assert(strcmp(out, "mp:external/DEF/https/example.com/b.png") == 0);
|
||||
assert(art_parse_mp(body, strlen(body),
|
||||
"https://example.com/zzz.png", out, sizeof out) == 0);
|
||||
assert(art_parse_mp("[]", 2, "x", out, sizeof out) == 0);
|
||||
assert(art_parse_mp(NULL, 0, "x", out, sizeof out) == 0);
|
||||
assert(art_resolve_mp("1", "t", "https://example.com/a.png", out, sizeof out) == 0);
|
||||
}
|
||||
|
||||
static void test_health_safe_mode(void) {
|
||||
/* Unclean-boot marker semantics: normal reboots never count. */
|
||||
char dir[64];
|
||||
assert(make_tmpdir(dir, sizeof dir) == 0);
|
||||
health_set_base(dir);
|
||||
health_mark_clean();
|
||||
/* clean boot x3: counter stays 0, never safe mode */
|
||||
assert(health_boot_note_crash() == 0);
|
||||
health_mark_healthy();
|
||||
assert(health_boot_note_crash() == 0);
|
||||
health_mark_healthy();
|
||||
assert(health_boot_note_crash() == 0);
|
||||
/* now crash repeatedly WITHOUT going healthy: 1, 2, then safe */
|
||||
assert(health_boot_note_crash() == 0); /* count=1 */
|
||||
assert(health_boot_note_crash() == 0); /* count=2 */
|
||||
assert(health_boot_note_crash() == 1); /* count=3 -> safe mode */
|
||||
/* recovery clears */
|
||||
health_mark_healthy();
|
||||
assert(health_boot_note_crash() == 0);
|
||||
health_mark_healthy();
|
||||
}
|
||||
|
||||
static void test_health_stage_activate(void) {
|
||||
/* Atomic staging: bad .new never touches live; rollback restores. */
|
||||
char dir[64];
|
||||
assert(make_tmpdir(dir, sizeof dir) == 0);
|
||||
health_set_base(dir);
|
||||
char live[256], tmp[256], bak[256];
|
||||
snprintf(live, sizeof live, "%s/live.bin", dir);
|
||||
snprintf(tmp, sizeof tmp, "%s/live.bin.new", dir);
|
||||
snprintf(bak, sizeof bak, "%s/live.bin.bak", dir);
|
||||
/* live = valid ELF stand-in (>=64B, ELF magic via updater_image_ok?
|
||||
* use real check: write 64 zero bytes won't pass; stage path only
|
||||
* needs .new validation, so craft minimal ELF header). */
|
||||
unsigned char elf[128];
|
||||
memset(elf, 0, sizeof elf);
|
||||
elf[0] = 0x7f; elf[1] = 'E'; elf[2] = 'L'; elf[3] = 'F';
|
||||
elf[4] = 2; elf[5] = 1; elf[18] = 62;
|
||||
FILE *f = fopen(live, "wb");
|
||||
assert(f); assert(fwrite(elf, 1, sizeof elf, f) == sizeof elf); fclose(f);
|
||||
/* corrupt .new is refused, live untouched */
|
||||
f = fopen(tmp, "wb");
|
||||
assert(f); assert(fwrite("garbage-not-elf-at-all......................"
|
||||
"..............................", 1, 64, f) == 64);
|
||||
fclose(f);
|
||||
assert(health_stage_activate(live) != 0);
|
||||
f = fopen(live, "rb");
|
||||
assert(f);
|
||||
unsigned char chk[4];
|
||||
assert(fread(chk, 1, 4, f) == 4);
|
||||
fclose(f);
|
||||
assert(chk[0] == 0x7f && chk[1] == 'E');
|
||||
/* valid .new activates, backup created, rollback restores */
|
||||
f = fopen(tmp, "wb");
|
||||
assert(f);
|
||||
elf[7] = 0x42;
|
||||
assert(fwrite(elf, 1, sizeof elf, f) == sizeof elf);
|
||||
fclose(f);
|
||||
assert(health_stage_activate(live) == 0);
|
||||
f = fopen(bak, "rb");
|
||||
assert(f); fclose(f);
|
||||
assert(health_verify_or_rollback(live) == 0);
|
||||
/* corrupt live + backup present -> rollback */
|
||||
f = fopen(live, "wb");
|
||||
assert(f); assert(fwrite("XX", 1, 2, f) == 2); fclose(f);
|
||||
assert(health_verify_or_rollback(live) == 1);
|
||||
f = fopen(live, "rb");
|
||||
assert(f);
|
||||
assert(fread(chk, 1, 4, f) == 4);
|
||||
fclose(f);
|
||||
assert(chk[0] == 0x7f);
|
||||
}
|
||||
|
||||
static void test_manifest(void) {
|
||||
const char *json = "{\"version\":\"1.0.0\",\"channel\":\"stable\","
|
||||
"\"min_version\":\"0.9.0\",\"platform\":\"ps4-goldhen\","
|
||||
"\"assets\":[{\"name\":\"orbisrpc.bin\","
|
||||
"\"sha256\":\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"}]}";
|
||||
manifest_t m;
|
||||
assert(manifest_parse(json, strlen(json), &m) == 0);
|
||||
assert(strcmp(m.version, "1.0.0") == 0);
|
||||
assert(strcmp(m.channel, "stable") == 0);
|
||||
char hex[65] = {0};
|
||||
assert(manifest_find(&m, "orbisrpc.bin", hex) == 0);
|
||||
assert(!strcmp(hex, "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"));
|
||||
assert(manifest_find(&m, "nope.bin", hex) != 0);
|
||||
assert(manifest_gate(&m) == 1);
|
||||
assert(manifest_is_newer(&m, "0.9.0") == 1);
|
||||
assert(manifest_is_newer(&m, "1.0.0") == 0);
|
||||
/* wrong channel / platform refused */
|
||||
const char *bad = "{\"version\":\"9.9.9\",\"channel\":\"beta\","
|
||||
"\"platform\":\"ps5\",\"assets\":[{\"name\":\"x.bin\","
|
||||
"\"sha256\":\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"}]}";
|
||||
manifest_t m2;
|
||||
assert(manifest_parse(bad, strlen(bad), &m2) == 0);
|
||||
assert(manifest_gate(&m2) == 0);
|
||||
/* malformed rejected */
|
||||
assert(manifest_parse("{}", 2, &m) != 0);
|
||||
assert(manifest_parse("not json", 8, &m) != 0);
|
||||
/* hash check: real sha256 of "abc" must match */
|
||||
const char *jh = "{\"version\":\"1\",\"assets\":[{\"name\":\"a\","
|
||||
"\"sha256\":\"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad\"}]}";
|
||||
manifest_t m3;
|
||||
assert(manifest_parse(jh, strlen(jh), &m3) == 0);
|
||||
assert(manifest_check(&m3, "a", (const unsigned char *)"abc", 3) == 0);
|
||||
assert(manifest_check(&m3, "a", (const unsigned char *)"abd", 3) != 0);
|
||||
}
|
||||
|
||||
static void test_manifest_sig(void) {
|
||||
/* Full round trip with a fresh keypair: sign via mbedTLS, verify via
|
||||
* our public-API-only manifest_verify_sig. Tampered bytes must fail.
|
||||
* Uses only public 3.x APIs (raw group + MPIs, no context internals). */
|
||||
static const unsigned char msg[] = "{\"version\":\"9.9.9\"}";
|
||||
mbedtls_entropy_context ent;
|
||||
mbedtls_entropy_init(&ent);
|
||||
mbedtls_ctr_drbg_context rng;
|
||||
mbedtls_ctr_drbg_init(&rng);
|
||||
assert(mbedtls_ctr_drbg_seed(&rng, mbedtls_entropy_func, &ent,
|
||||
(const unsigned char *)"test", 4) == 0);
|
||||
mbedtls_ecp_group grp;
|
||||
mbedtls_ecp_group_init(&grp);
|
||||
assert(mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1) == 0);
|
||||
mbedtls_mpi d, r, s;
|
||||
mbedtls_mpi_init(&d); mbedtls_mpi_init(&r); mbedtls_mpi_init(&s);
|
||||
mbedtls_ecp_point Q;
|
||||
mbedtls_ecp_point_init(&Q);
|
||||
assert(mbedtls_ecp_gen_keypair(&grp, &d, &Q,
|
||||
mbedtls_ctr_drbg_random, &rng) == 0);
|
||||
unsigned char hash[32], sig[64], rawpub[64];
|
||||
{
|
||||
mbedtls_sha256_context sc;
|
||||
mbedtls_sha256_init(&sc);
|
||||
assert(mbedtls_sha256_starts(&sc, 0) == 0);
|
||||
assert(mbedtls_sha256_update(&sc, msg, sizeof msg - 1) == 0);
|
||||
assert(mbedtls_sha256_finish(&sc, hash) == 0);
|
||||
mbedtls_sha256_free(&sc);
|
||||
}
|
||||
assert(mbedtls_ecdsa_sign(&grp, &r, &s, &d, hash, sizeof hash,
|
||||
mbedtls_ctr_drbg_random, &rng) == 0);
|
||||
assert(mbedtls_mpi_write_binary(&r, sig, 32) == 0);
|
||||
assert(mbedtls_mpi_write_binary(&s, sig + 32, 32) == 0);
|
||||
/* export X||Y via the public point-write API */
|
||||
{
|
||||
unsigned char uncomp[65];
|
||||
size_t olen = 0;
|
||||
assert(mbedtls_ecp_point_write_binary(&grp, &Q,
|
||||
MBEDTLS_ECP_PF_UNCOMPRESSED, &olen, uncomp, sizeof uncomp) == 0);
|
||||
assert(olen == 65 && uncomp[0] == 0x04);
|
||||
memcpy(rawpub, uncomp + 1, 64);
|
||||
}
|
||||
assert(manifest_verify_sig(msg, sizeof msg - 1, sig, rawpub) == 0);
|
||||
sig[10] ^= 0x01;
|
||||
assert(manifest_verify_sig(msg, sizeof msg - 1, sig, rawpub) != 0);
|
||||
sig[10] ^= 0x01;
|
||||
unsigned char bad[sizeof msg];
|
||||
memcpy(bad, msg, sizeof bad);
|
||||
bad[5] ^= 0x01;
|
||||
assert(manifest_verify_sig(bad, sizeof bad - 1, sig, rawpub) != 0);
|
||||
assert(manifest_verify_sig(NULL, 0, sig, rawpub) != 0);
|
||||
mbedtls_mpi_free(&d); mbedtls_mpi_free(&r); mbedtls_mpi_free(&s);
|
||||
mbedtls_ecp_point_free(&Q);
|
||||
mbedtls_ecp_group_free(&grp);
|
||||
mbedtls_ctr_drbg_free(&rng);
|
||||
mbedtls_entropy_free(&ent);
|
||||
}
|
||||
|
||||
static void test_base64(void) {
|
||||
char out[32];
|
||||
assert(b64_encode((const unsigned char *)"", 0, out) == 0);
|
||||
assert(strcmp(out, "") == 0);
|
||||
assert(b64_encode((const unsigned char *)"f", 1, out) == 4);
|
||||
assert(strcmp(out, "Zg==") == 0);
|
||||
assert(b64_encode((const unsigned char *)"fo", 2, out) == 4);
|
||||
assert(strcmp(out, "Zm8=") == 0);
|
||||
assert(b64_encode((const unsigned char *)"foo", 3, out) == 4);
|
||||
assert(strcmp(out, "Zm9v") == 0);
|
||||
}
|
||||
|
||||
static void test_cfg_titles(void) {
|
||||
char dir[64], path[96], path2[96];
|
||||
assert(make_tmpdir(dir, sizeof dir) == 0);
|
||||
snprintf(path, sizeof path, "%s/cfg.json", dir);
|
||||
snprintf(path2, sizeof path2, "%s/cfg2.json", dir);
|
||||
FILE *f = fopen(path, "wb");
|
||||
assert(f);
|
||||
fputs("{\"token\":\"t\",\"titles\":{\"CUSA11995\":\"Marvel's Spider-Man\","
|
||||
"\"BAD KEY!\": \"junk\", \"CUSA00001\": \"x\", \"TOOLONGTITLEID12345\": \"y\","
|
||||
"\"CUSA00002\": 42}}", f);
|
||||
fclose(f);
|
||||
cfg_t c;
|
||||
assert(cfg_load(path, &c) == 0);
|
||||
assert(c.n_titles == 1); /* only the well-formed entry survives */
|
||||
char name[128];
|
||||
assert(cfg_title(&c, "CUSA11995", name, sizeof name) == 0);
|
||||
assert(!strcmp(name, "Marvel's Spider-Man"));
|
||||
assert(cfg_title(&c, "CUSA99999", name, sizeof name) != 0);
|
||||
assert(cfg_title(&c, "", name, sizeof name) != 0);
|
||||
assert(cfg_title(NULL, "CUSA11995", name, sizeof name) != 0);
|
||||
/* round-trip: save preserves overrides + home_art */
|
||||
strncpy(c.home_art, "pslogo", sizeof c.home_art - 1);
|
||||
assert(cfg_save(path2, &c) == 0);
|
||||
cfg_t c2;
|
||||
assert(cfg_load(path2, &c2) == 0);
|
||||
assert(c2.n_titles == 1);
|
||||
assert(cfg_title(&c2, "CUSA11995", name, sizeof name) == 0);
|
||||
assert(!strcmp(name, "Marvel's Spider-Man"));
|
||||
assert(!strcmp(c2.home_art, "pslogo"));
|
||||
/* missing titles object: zero overrides, lookup misses */
|
||||
f = fopen(path, "wb");
|
||||
assert(f);
|
||||
fputs("{\"token\":\"t\"}", f);
|
||||
fclose(f);
|
||||
assert(cfg_load(path, &c) == 0);
|
||||
assert(c.n_titles == 0);
|
||||
assert(!strcmp(c.home_art, "https://raw.githubusercontent.com/SirHumza/orbisRPC/main/config/icons/logo.png"));
|
||||
}
|
||||
|
||||
static void test_appdb(void) {
|
||||
char dir[64], db[96], meta[96], pj[128];
|
||||
assert(make_tmpdir(dir, sizeof dir) == 0);
|
||||
snprintf(db, sizeof db, "%s/app.db", dir);
|
||||
snprintf(meta, sizeof meta, "%s/appmeta", dir);
|
||||
assert(mkdir(meta, 0700) == 0);
|
||||
sqlite3 *s = NULL;
|
||||
assert(sqlite3_open_v2(db, &s, SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE|SQLITE_OPEN_NOMUTEX, NULL) == SQLITE_OK);
|
||||
assert(sqlite3_exec(s, "CREATE TABLE tbl_appbrowse(titleId TEXT, titleName TEXT);", 0, 0, 0) == SQLITE_OK);
|
||||
assert(sqlite3_exec(s, "INSERT INTO tbl_appbrowse VALUES('CUSA11995','Marvel''s Spider-Man');", 0, 0, 0) == SQLITE_OK);
|
||||
assert(sqlite3_exec(s, "CREATE TABLE tbl_appinfo(titleId TEXT, key TEXT, val TEXT);", 0, 0, 0) == SQLITE_OK);
|
||||
assert(sqlite3_exec(s, "INSERT INTO tbl_appinfo VALUES('CUSA00001','TITLE_01','Lang One');", 0, 0, 0) == SQLITE_OK);
|
||||
assert(sqlite3_exec(s, "INSERT INTO tbl_appinfo VALUES('CUSA00001','TITLE','Base Name');", 0, 0, 0) == SQLITE_OK);
|
||||
assert(sqlite3_exec(s, "INSERT INTO tbl_appinfo VALUES('CUSA00002','TITLE','');", 0, 0, 0) == SQLITE_OK);
|
||||
sqlite3_close(s);
|
||||
char name[128];
|
||||
/* tier 1: browse name wins */
|
||||
assert(appdb_title_from(db, meta, "CUSA11995", name, sizeof name) == 0);
|
||||
assert(!strcmp(name, "Marvel's Spider-Man"));
|
||||
/* tier 2: bare TITLE preferred over TITLE_01 */
|
||||
assert(appdb_title_from(db, meta, "CUSA00001", name, sizeof name) == 0);
|
||||
assert(!strcmp(name, "Base Name"));
|
||||
/* empty val rows never match; no param.json staged -> miss */
|
||||
assert(appdb_title_from(db, meta, "CUSA00002", name, sizeof name) != 0);
|
||||
assert(appdb_title_from(db, meta, "CUSA99999", name, sizeof name) != 0);
|
||||
/* tier 3: staged param.json */
|
||||
char gdir[128];
|
||||
snprintf(gdir, sizeof gdir, "%s/CUSA00002", meta);
|
||||
assert(mkdir(gdir, 0700) == 0);
|
||||
snprintf(pj, sizeof pj, "%s/param.json", gdir);
|
||||
FILE *f = fopen(pj, "wb");
|
||||
assert(f);
|
||||
fputs("{\"titleId\":\"CUSA00002\",\"titleName\":\"JSON Game\"}", f);
|
||||
fclose(f);
|
||||
assert(appdb_title_from(db, meta, "CUSA00002", name, sizeof name) == 0);
|
||||
assert(!strcmp(name, "JSON Game"));
|
||||
/* invalid inputs fail soft */
|
||||
assert(appdb_title_from(db, meta, NULL, name, sizeof name) != 0);
|
||||
assert(appdb_title_from(db, meta, "short", name, sizeof name) != 0);
|
||||
assert(appdb_title_from(db, meta, "cusa11995", name, sizeof name) != 0);
|
||||
assert(appdb_title_from(db, meta, "CUSA1199!", name, sizeof name) != 0);
|
||||
assert(appdb_title_from("/nonexistent/app.db", meta, "CUSA11995", name, sizeof name) != 0);
|
||||
assert(appdb_title_from(db, meta, "CUSA11995", NULL, 0) != 0);
|
||||
}
|
||||
|
||||
static void test_discord_builder(void) {
|
||||
/* game presence: name shown, raw ID nowhere visible, hover has the ID */
|
||||
jl_val_t *a = discord_build_activity("On PS4", "Marvel's Spider-Man",
|
||||
"CUSA11995", "1536977374795538532", "https://x/icons/", NULL, NULL,
|
||||
1700000000LL, "");
|
||||
assert(a);
|
||||
const jl_val_t *v = jl_obj_get(a, "name");
|
||||
assert(v && v->type == JL_STRING && !strcmp(v->str, "Marvel's Spider-Man"));
|
||||
assert(jl_obj_get(a, "details") == NULL); /* never the raw ID */
|
||||
v = jl_obj_get(a, "state");
|
||||
assert(v && v->type == JL_STRING && !strcmp(v->str, "On PS4"));
|
||||
v = jl_obj_get(a, "type");
|
||||
assert(v && v->type == JL_NUMBER && (int)v->num == 0);
|
||||
const jl_val_t *ts = jl_obj_get(a, "timestamps");
|
||||
assert(ts && ts->type == JL_OBJECT);
|
||||
v = jl_obj_get(ts, "start");
|
||||
assert(v && v->type == JL_NUMBER && v->inum == 1700000000LL * 1000LL);
|
||||
const jl_val_t *as = jl_obj_get(a, "assets");
|
||||
assert(as && as->type == JL_OBJECT);
|
||||
v = jl_obj_get(as, "large_image");
|
||||
assert(v && v->type == JL_STRING && !strcmp(v->str, "cusa11995"));
|
||||
v = jl_obj_get(as, "large_text");
|
||||
assert(v && v->type == JL_STRING && !strcmp(v->str, "CUSA11995"));
|
||||
assert(jl_obj_get(as, "small_image") == NULL); /* no badge requested */
|
||||
jl_free(a);
|
||||
/* system badge: mp: URL used as-is with platform hover text */
|
||||
a = discord_build_activity("On PS4", "Marvel's Spider-Man",
|
||||
"CUSA11995", "1536977374795538532", NULL, NULL, "mp:1/2/logo",
|
||||
1700000000LL, "");
|
||||
assert(a);
|
||||
as = jl_obj_get(a, "assets");
|
||||
assert(as && as->type == JL_OBJECT);
|
||||
v = jl_obj_get(as, "small_image");
|
||||
assert(v && v->type == JL_STRING && !strcmp(v->str, "mp:1/2/logo"));
|
||||
v = jl_obj_get(as, "small_text");
|
||||
assert(v && v->type == JL_STRING && !strcmp(v->str, "PlayStation 4"));
|
||||
jl_free(a);
|
||||
/* media type mapping survives */
|
||||
a = discord_build_activity(NULL, "YouTube", "CUSA01015", "app",
|
||||
NULL, NULL, NULL, 0, "");
|
||||
assert(a);
|
||||
v = jl_obj_get(a, "type");
|
||||
assert(v && (int)v->num == 0); /* stub maps only CUSA00127 */
|
||||
jl_free(a);
|
||||
a = discord_build_activity(NULL, "Netflix", "CUSA00127", "app",
|
||||
NULL, NULL, NULL, 0, "");
|
||||
assert(a);
|
||||
v = jl_obj_get(a, "type");
|
||||
assert(v && (int)v->num == 3);
|
||||
jl_free(a);
|
||||
/* home + uploaded key: trusted key used as-is */
|
||||
a = discord_build_activity("On PS4", "PlayStation 4", "home", "app",
|
||||
NULL, "pslogo", NULL, 0, "");
|
||||
assert(a);
|
||||
as = jl_obj_get(a, "assets");
|
||||
assert(as && as->type == JL_OBJECT);
|
||||
v = jl_obj_get(as, "large_image");
|
||||
assert(v && !strcmp(v->str, "pslogo"));
|
||||
v = jl_obj_get(as, "large_text");
|
||||
assert(v && !strcmp(v->str, "PlayStation 4"));
|
||||
jl_free(a);
|
||||
/* home with nothing: no assets block at all (never dangling) */
|
||||
a = discord_build_activity("On PS4", "PlayStation 4", "home", "app",
|
||||
NULL, NULL, NULL, 0, "");
|
||||
assert(a);
|
||||
assert(jl_obj_get(a, "assets") == NULL);
|
||||
jl_free(a);
|
||||
/* NULL name rejected */
|
||||
assert(discord_build_activity(NULL, NULL, "home", "app", NULL, NULL, NULL, 0, "") == NULL);
|
||||
}
|
||||
|
||||
static void test_cfg_learn(void) {
|
||||
cfg_t c;
|
||||
cfg_defaults(&c);
|
||||
assert(c.n_titles == 0);
|
||||
assert(cfg_learn(&c, "CUSA11995", "Marvel's Spider-Man") == 1);
|
||||
assert(cfg_learn(&c, "CUSA11995", "Marvel's Spider-Man") == 0); /* dup */
|
||||
assert(cfg_learn(&c, "CUSA11995", "CUSA11995") == 0); /* ID echo refused */
|
||||
assert(cfg_learn(&c, "CUSA11995", "x") == 0); /* too short */
|
||||
assert(cfg_learn(&c, "bad key!", "Name") == 0);
|
||||
assert(cfg_learn(&c, "CUSA11995", "Spider-Man 2") == 1); /* update */
|
||||
char name[128];
|
||||
assert(cfg_title(&c, "CUSA11995", name, sizeof name) == 0);
|
||||
assert(!strcmp(name, "Spider-Man 2"));
|
||||
assert(cfg_learn(NULL, "CUSA11995", "N") == 0);
|
||||
assert(cfg_learn(&c, NULL, "N") == 0);
|
||||
/* full map refuses */
|
||||
c.n_titles = CFG_MAX_TITLES;
|
||||
assert(cfg_learn(&c, "CUSA00001", "Full Game") == 0);
|
||||
}
|
||||
|
||||
static void test_installer_cfg(void) {
|
||||
char dir[64], path[96];
|
||||
assert(make_tmpdir(dir, sizeof dir) == 0);
|
||||
snprintf(path, sizeof path, "%s/config.json", dir);
|
||||
/* token validation */
|
||||
assert(token_valid("MTIz.ABCdef_0123456789-xYz.ABCDEFGHijklmnopQRSTUVwxyz") == 1);
|
||||
assert(token_valid("short") == 0);
|
||||
assert(token_valid("SET_ME") == 0);
|
||||
assert(token_valid("has space.in.it.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") == 0);
|
||||
assert(token_valid("nodots") == 0);
|
||||
assert(token_valid(NULL) == 0);
|
||||
/* save creates, preserves, round-trips */
|
||||
assert(icfg_token_save(path, "MTIz.ABCdef_0123456789-xYz.ABCDEFGHijklmnopQRSTUVwxyz") == 0);
|
||||
assert(icfg_set_str(path, "presence_state", "On PS4") == 0);
|
||||
assert(icfg_set_int(path, "poll_interval_s", 12) == 0);
|
||||
/* invalid token refused, file untouched */
|
||||
assert(icfg_token_save(path, "junk") == -1);
|
||||
char tok[160], st[64];
|
||||
long n = 0;
|
||||
assert(icfg_token_load(path, tok, sizeof tok) == 0);
|
||||
assert(!strcmp(tok, "MTIz.ABCdef_0123456789-xYz.ABCDEFGHijklmnopQRSTUVwxyz"));
|
||||
assert(icfg_get_str(path, "presence_state", st, sizeof st) == 0);
|
||||
assert(!strcmp(st, "On PS4"));
|
||||
assert(icfg_get_int(path, "poll_interval_s", &n) == 0 && n == 12);
|
||||
/* missing file: loads fail soft, save still works */
|
||||
assert(icfg_get_str("/nonexistent/x.json", "k", st, sizeof st) != 0);
|
||||
/* net probes fail soft on garbage */
|
||||
assert(net_probe(NULL, 443, 2) == 0);
|
||||
assert(net_probe("", 443, 2) == 0);
|
||||
assert(net_probe("127.0.0.1", 1, 1) == 0);
|
||||
assert(net_probe("nonexistent.invalid", 443, 1) == 0);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_json();
|
||||
test_gateway_op_spoof();
|
||||
test_json_oom_safe();
|
||||
test_json_hostile();
|
||||
test_tmdb();
|
||||
test_updater();
|
||||
test_sfo();
|
||||
test_base64();
|
||||
test_art_parse();
|
||||
test_health_safe_mode();
|
||||
test_health_stage_activate();
|
||||
test_manifest();
|
||||
test_manifest_sig();
|
||||
test_cfg_titles();
|
||||
test_cfg_learn();
|
||||
test_installer_cfg();
|
||||
test_appdb();
|
||||
test_discord_builder();
|
||||
puts("utility tests passed");
|
||||
return 0;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
Copyright (c) 2016 Thomas Pornin <pornin@bolet.org>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Vendored
+183
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright (c) 2016 Thomas Pornin <pornin@bolet.org>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef BR_BEARSSL_H__
|
||||
#define BR_BEARSSL_H__
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/** \mainpage BearSSL API
|
||||
*
|
||||
* # API Layout
|
||||
*
|
||||
* The functions and structures defined by the BearSSL API are located
|
||||
* in various header files:
|
||||
*
|
||||
* | Header file | Elements |
|
||||
* | :-------------- | :------------------------------------------------ |
|
||||
* | bearssl_hash.h | Hash functions |
|
||||
* | bearssl_hmac.h | HMAC |
|
||||
* | bearssl_kdf.h | Key Derivation Functions |
|
||||
* | bearssl_rand.h | Pseudorandom byte generators |
|
||||
* | bearssl_prf.h | PRF implementations (for SSL/TLS) |
|
||||
* | bearssl_block.h | Symmetric encryption |
|
||||
* | bearssl_aead.h | AEAD algorithms (combined encryption + MAC) |
|
||||
* | bearssl_rsa.h | RSA encryption and signatures |
|
||||
* | bearssl_ec.h | Elliptic curves support (including ECDSA) |
|
||||
* | bearssl_ssl.h | SSL/TLS engine interface |
|
||||
* | bearssl_x509.h | X.509 certificate decoding and validation |
|
||||
* | bearssl_pem.h | Base64/PEM decoding support functions |
|
||||
*
|
||||
* Applications using BearSSL are supposed to simply include `bearssl.h`
|
||||
* as follows:
|
||||
*
|
||||
* #include <bearssl.h>
|
||||
*
|
||||
* The `bearssl.h` file itself includes all the other header files. It is
|
||||
* possible to include specific header files, but it has no practical
|
||||
* advantage for the application. The API is separated into separate
|
||||
* header files only for documentation convenience.
|
||||
*
|
||||
*
|
||||
* # Conventions
|
||||
*
|
||||
* ## MUST and SHALL
|
||||
*
|
||||
* In all descriptions, the usual "MUST", "SHALL", "MAY",... terminology
|
||||
* is used. Failure to meet requirements expressed with a "MUST" or
|
||||
* "SHALL" implies undefined behaviour, which means that segmentation
|
||||
* faults, buffer overflows, and other similar adverse events, may occur.
|
||||
*
|
||||
* In general, BearSSL is not very forgiving of programming errors, and
|
||||
* does not include much failsafes or error reporting when the problem
|
||||
* does not arise from external transient conditions, and can be fixed
|
||||
* only in the application code. This is done so in order to make the
|
||||
* total code footprint lighter.
|
||||
*
|
||||
*
|
||||
* ## `NULL` values
|
||||
*
|
||||
* Function parameters with a pointer type shall not be `NULL` unless
|
||||
* explicitly authorised by the documentation. As an exception, when
|
||||
* the pointer aims at a sequence of bytes and is accompanied with
|
||||
* a length parameter, and the length is zero (meaning that there is
|
||||
* no byte at all to retrieve), then the pointer may be `NULL` even if
|
||||
* not explicitly allowed.
|
||||
*
|
||||
*
|
||||
* ## Memory Allocation
|
||||
*
|
||||
* BearSSL does not perform dynamic memory allocation. This implies that
|
||||
* for any functionality that requires a non-transient state, the caller
|
||||
* is responsible for allocating the relevant context structure. Such
|
||||
* allocation can be done in any appropriate area, including static data
|
||||
* segments, the heap, and the stack, provided that proper alignment is
|
||||
* respected. The header files define these context structures
|
||||
* (including size and contents), so the C compiler should handle
|
||||
* alignment automatically.
|
||||
*
|
||||
* Since there is no dynamic resource allocation, there is also nothing to
|
||||
* release. When the calling code is done with a BearSSL feature, it
|
||||
* may simple release the context structures it allocated itself, with
|
||||
* no "close function" to call. If the context structures were allocated
|
||||
* on the stack (as local variables), then even that release operation is
|
||||
* implicit.
|
||||
*
|
||||
*
|
||||
* ## Structure Contents
|
||||
*
|
||||
* Except when explicitly indicated, structure contents are opaque: they
|
||||
* are included in the header files so that calling code may know the
|
||||
* structure sizes and alignment requirements, but callers SHALL NOT
|
||||
* access individual fields directly. For fields that are supposed to
|
||||
* be read from or written to, the API defines accessor functions (the
|
||||
* simplest of these accessor functions are defined as `static inline`
|
||||
* functions, and the C compiler will optimise them away).
|
||||
*
|
||||
*
|
||||
* # API Usage
|
||||
*
|
||||
* BearSSL usage for running a SSL/TLS client or server is described
|
||||
* on the [BearSSL Web site](https://www.bearssl.org/api1.html). The
|
||||
* BearSSL source archive also comes with sample code.
|
||||
*/
|
||||
|
||||
#include "bearssl_hash.h"
|
||||
#include "bearssl_hmac.h"
|
||||
#include "bearssl_kdf.h"
|
||||
#include "bearssl_rand.h"
|
||||
#include "bearssl_prf.h"
|
||||
#include "bearssl_block.h"
|
||||
#include "bearssl_aead.h"
|
||||
#include "bearssl_rsa.h"
|
||||
#include "bearssl_ec.h"
|
||||
#include "bearssl_ssl.h"
|
||||
#include "bearssl_x509.h"
|
||||
#include "bearssl_pem.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** \brief Type for a configuration option.
|
||||
*
|
||||
* A "configuration option" is a value that is selected when the BearSSL
|
||||
* library itself is compiled. Most options are boolean; their value is
|
||||
* then either 1 (option is enabled) or 0 (option is disabled). Some
|
||||
* values have other integer values. Option names correspond to macro
|
||||
* names. Some of the options can be explicitly set in the internal
|
||||
* `"config.h"` file.
|
||||
*/
|
||||
typedef struct {
|
||||
/** \brief Configurable option name. */
|
||||
const char *name;
|
||||
/** \brief Configurable option value. */
|
||||
long value;
|
||||
} br_config_option;
|
||||
|
||||
/** \brief Get configuration report.
|
||||
*
|
||||
* This function returns compiled configuration options, each as a
|
||||
* 'long' value. Names match internal macro names, in particular those
|
||||
* that can be set in the `"config.h"` inner file. For boolean options,
|
||||
* the numerical value is 1 if enabled, 0 if disabled. For maximum
|
||||
* key sizes, values are expressed in bits.
|
||||
*
|
||||
* The returned array is terminated by an entry whose `name` is `NULL`.
|
||||
*
|
||||
* \return the configuration report.
|
||||
*/
|
||||
const br_config_option *br_get_config(void);
|
||||
|
||||
/* ======================================================================= */
|
||||
|
||||
/** \brief Version feature: support for time callback. */
|
||||
#define BR_FEATURE_X509_TIME_CALLBACK 1
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
+1059
File diff suppressed because it is too large.
Load diff
Loaded 100 of 694 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user