Files
MoHadiShibli--Control4Free/src/net.c
T
MoHadiShibli 6d931a05ce Payload: system-wide virtual controllers through Sony's VDA API
A GoldHEN ELF payload that creates virtual DualShock 4 controllers with
scePadVirtualDevice*, the path Remote Play uses, so they work on the home
screen, at sign-in and in games. A phone or PC drives them from a page the
payload serves on port 4264.

- Stage 0 (default) is the browser controller: four slots, explicit
  creation, native PS4 user selection, input over a WebSocket.
- Stages 1-8 are the diagnostics that established the VDA call order.
- Loopback-only /api/status and /api/stop for the launcher app.
- The klog connection is reopened when a controller is created, so a
  start through GoldHEN AutoRun still works if it beats the klog server.

The VDA code is ported from seregonwar/SplashDown (GPL-3.0).
2026-10-04 19:53:43 +03:00

344 lines
15 KiB
C

/* Bounded HTTP/WebSocket transport for the embedded controller page. */
#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#include "c4f_net.h"
uint64_t c4fTimeMs(void)
{
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return (uint64_t)t.tv_sec * 1000 + (uint64_t)t.tv_nsec / 1000000;
}
static uint32_t c4fRol(uint32_t v, unsigned n) { return (v << n) | (v >> (32 - n)); }
/* RFC 6455's SHA-1 + base64 handshake. Keys are fixed at 24 ASCII bytes; this
* implementation hashes only the key and protocol GUID, never arbitrary data. */
void c4fWebSocketAccept(const char *key, char out[29])
{
unsigned char msg[128] = {0}, digest[20];
uint32_t h[5] = {0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0};
static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
unsigned block, i, j = 0;
memcpy(msg, key, 24);
memcpy(msg + 24, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", 36);
msg[60] = 0x80;
msg[126] = 1; msg[127] = 0xe0; /* 60 * 8 bits */
for (block = 0; block < 2; block++) {
uint32_t w[80], a = h[0], b = h[1], c = h[2], d = h[3], e = h[4];
for (i = 0; i < 16; i++) {
const unsigned char *p = msg + block * 64 + i * 4;
w[i] = (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3];
}
for (i = 16; i < 80; i++) w[i] = c4fRol(w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16], 1);
for (i = 0; i < 80; i++) {
uint32_t f, k, t;
if (i < 20) { f = (b & c) | (~b & d); k = 0x5a827999; }
else if (i < 40) { f = b ^ c ^ d; k = 0x6ed9eba1; }
else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8f1bbcdc; }
else { f = b ^ c ^ d; k = 0xca62c1d6; }
t = c4fRol(a, 5) + f + e + k + w[i];
e = d; d = c; c = c4fRol(b, 30); b = a; a = t;
}
h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e;
}
for (i = 0; i < 20; i++) digest[i] = (unsigned char)(h[i/4] >> (24 - 8*(i%4)));
for (i = 0; i < 20; i += 3) {
uint32_t v = (uint32_t)digest[i] << 16;
if (i+1 < 20) v |= (uint32_t)digest[i+1] << 8;
if (i+2 < 20) v |= digest[i+2];
out[j++] = b64[v >> 18]; out[j++] = b64[(v >> 12) & 63];
out[j++] = i+1 < 20 ? b64[(v >> 6) & 63] : '=';
out[j++] = i+2 < 20 ? b64[v & 63] : '=';
}
out[j] = 0;
}
static int c4fQueue(C4fNetClient *c, const void *data, size_t n)
{
if (c->closing || c->body) return -1;
if (c->txSent) {
memmove(c->tx, c->tx + c->txSent, c->txUsed - c->txSent);
c->txUsed -= c->txSent; c->txSent = 0;
}
if (n > sizeof(c->tx) - c->txUsed) { c->closing = 1; return -1; }
if (n) memcpy(c->tx + c->txUsed, data, n);
c->txUsed += n;
return 0;
}
static int c4fFrame(C4fNetClient *c, unsigned op, const void *data, size_t n)
{
unsigned char head[4] = { (unsigned char)(0x80 | op), (unsigned char)n, 0, 0 };
size_t h = 2;
if (n >= 126) { head[1] = 126; head[2] = n >> 8; head[3] = n; h = 4; }
if (c4fQueue(c, head, h) != 0) return -1;
return c4fQueue(c, data, n);
}
int c4fNetText(C4fNetClient *c, const char *text) { return c4fFrame(c, 1, text, strlen(text)); }
void c4fNetHttpJson(C4fNetClient *c, const char *json)
{
char header[256];
int n = snprintf(header, sizeof(header), "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: %zu\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n", strlen(json));
if (c4fQueue(c, header, (size_t)n) || c4fQueue(c, json, strlen(json))) return;
c->closing = 2;
}
static void c4fDrop(C4fNet *net, C4fNetClient *c)
{
if (c->websocket) net->handler(c, C4F_NET_CLOSE, NULL, 0, net->context);
close(c->fd);
memset(c, 0, sizeof(*c)); c->fd = -1;
}
/* Header values are copied and duplicates are rejected. */
static int c4fHeader(const char *request, const char *key, char *value, size_t size)
{
const char *p = strstr(request, "\r\n");
size_t k = strlen(key);
int found = 0;
value[0] = 0;
while (p && p[2] && p[2] != '\r') {
const char *end, *start;
p += 2; end = strstr(p, "\r\n");
if (!end) return -1;
if ((size_t)(end - p) > k && !strncasecmp(p, key, k) && p[k] == ':') {
if (found++) return -1;
start = p + k + 1;
while (start < end && (*start == ' ' || *start == '\t')) start++;
while (end > start && (end[-1] == ' ' || end[-1] == '\t')) end--;
if ((size_t)(end-start) >= size) return -1;
memcpy(value, start, end-start); value[end-start] = 0;
}
p = strstr(p, "\r\n");
}
return found;
}
static void c4fHttpError(C4fNetClient *c, int code, const char *message)
{
char buf[512];
int n = snprintf(buf, sizeof(buf), "HTTP/1.1 %d Error\r\nConnection: close\r\nContent-Type: text/plain\r\nContent-Length: %zu\r\n\r\n%s", code, strlen(message), message);
c4fQueue(c, buf, (size_t)n); c->closing = 2;
}
static void c4fHttp(C4fNet *net, C4fNetClient *c)
{
char request[8193], host[128], origin[160], key[40], upgrade[32], version[12], connection[128];
char *end;
size_t used;
memcpy(request, c->rx, c->rxUsed); request[c->rxUsed] = 0;
end = strstr(request, "\r\n\r\n");
if (!end) { if (c->rxUsed == sizeof(c->rx)) c4fHttpError(c, 431, "Headers too large"); return; }
used = (size_t)(end-request) + 4;
request[used] = 0;
int statusRequest = !strncmp(request, "GET /api/status HTTP/1.1\r\n", 26);
int stopRequest = !strncmp(request, "POST /api/stop HTTP/1.1\r\n", 25);
if (!statusRequest && !stopRequest && strncmp(request, "GET / HTTP/1.1\r\n", 16) &&
strncmp(request, "GET /index.html HTTP/1.1\r\n", 26) &&
strncmp(request, "GET /ws HTTP/1.1\r\n", 18) &&
strncmp(request, "GET /?", 6)) {
c4fHttpError(c, 404, "Not found"); return;
}
if (c4fHeader(request, "Host", host, sizeof(host)) != 1 ||
c4fHeader(request, "Origin", origin, sizeof(origin)) < 0 ||
c4fHeader(request, "Upgrade", upgrade, sizeof(upgrade)) < 0) {
c4fHttpError(c, 400, "Invalid headers"); return;
}
/* Serve only literal IPv4 addresses or localhost: block DNS-rebinding hosts. */
{
char address[128], *colon; struct in_addr parsed;
snprintf(address, sizeof(address), "%s", host);
colon = strchr(address, ':'); if (colon) *colon = 0;
if (strcmp(address, "localhost") && inet_pton(AF_INET, address, &parsed) != 1) {
c4fHttpError(c, 403, "Open the PS4 IP address"); return;
}
}
if (statusRequest || stopRequest) {
char launcher[8], length[16], transfer[32];
/* Management is local to the console. Reject browser-origin requests
* and require a custom header, so a website cannot trigger shutdown. */
if (!c->loopback || origin[0] || upgrade[0] ||
c4fHeader(request, "X-Control4Free-Launcher", launcher, sizeof(launcher)) != 1 || strcmp(launcher, "1") ||
c4fHeader(request, "Content-Length", length, sizeof(length)) < 0 || (length[0] && strcmp(length, "0")) ||
c4fHeader(request, "Transfer-Encoding", transfer, sizeof(transfer)) != 0) {
c4fHttpError(c, 403, "Local launcher only"); return;
}
c->rxUsed = 0;
net->handler(c, stopRequest ? C4F_NET_HTTP_STOP : C4F_NET_HTTP_STATUS, NULL, 0, net->context);
} else if (upgrade[0]) {
char accept[29], reply[256], expected[160];
int n;
snprintf(expected, sizeof(expected), "http://%s", host);
if (origin[0] && strcmp(origin, "null") && strcmp(origin, expected)) {
c4fHttpError(c, 403, "Origin not allowed"); return;
}
if (strcasecmp(upgrade, "websocket") ||
c4fHeader(request, "Sec-WebSocket-Key", key, sizeof(key)) != 1 ||
c4fHeader(request, "Sec-WebSocket-Version", version, sizeof(version)) != 1 ||
c4fHeader(request, "Connection", connection, sizeof(connection)) != 1 ||
strcmp(version, "13") || strlen(key) != 24 || strcmp(key+22, "==") ||
strspn(key, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") != 22) {
c4fHttpError(c, 400, "Invalid WebSocket handshake"); return;
}
for (size_t i = 0; connection[i]; i++) connection[i] = (char)tolower((unsigned char)connection[i]);
if (!strstr(connection, "upgrade")) { c4fHttpError(c, 400, "Upgrade required"); return; }
c4fWebSocketAccept(key, accept);
n = snprintf(reply, sizeof(reply), "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: %s\r\n\r\n", accept);
c4fQueue(c, reply, (size_t)n); c->websocket = 1;
memmove(c->rx, c->rx+used, c->rxUsed-used); c->rxUsed -= used;
net->handler(c, C4F_NET_OPEN, NULL, 0, net->context);
} else {
char reply[768];
int n = snprintf(reply, sizeof(reply), "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Encoding: gzip\r\nContent-Length: %zu\r\nConnection: close\r\nCache-Control: no-store\r\nX-Content-Type-Options: nosniff\r\nReferrer-Policy: no-referrer\r\nContent-Security-Policy: default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; media-src data:; connect-src ws:; frame-ancestors 'none'; base-uri 'none'; form-action 'none'\r\n\r\n", c4fPageSize);
c4fQueue(c, reply, (size_t)n);
c->body = c4fPage; c->bodySize = c4fPageSize; c->rxUsed = 0;
}
}
static void c4fWs(C4fNet *net, C4fNetClient *c)
{
while (c->rxUsed >= 2 && !c->closing) {
unsigned char *p = c->rx;
unsigned op = p[0] & 15, fin = p[0] >> 7;
size_t n = p[1] & 127, head = 2;
if ((p[0] & 0x70) || !(p[1] & 128)) { c->closing = 1; break; }
if (n == 126) {
if (c->rxUsed < 4) break;
n = (size_t)p[2] << 8 | p[3]; head = 4;
if (n < 126) { c->closing = 1; break; }
} else if (n == 127) { c->closing = 1; break; } /* messages are capped at 4 KiB */
if (n > C4F_NET_MESSAGE || (op >= 8 && (!fin || n > 125))) { c->closing = 1; break; }
if (c->rxUsed < head + 4 + n) break;
unsigned char *mask = p + head, *data = mask + 4;
for (size_t i = 0; i < n; i++) data[i] ^= mask[i % 4];
if (op == 8) {
c4fFrame(c, 8, NULL, 0); c->closing = 2;
} else if (op == 9) c4fFrame(c, 10, data, n);
else if (op == 10) { /* pong */ }
else if ((op == 1 && !c->fragmented) || (op == 0 && c->fragmented)) {
if (n > C4F_NET_MESSAGE - c->messageUsed) { c->closing = 1; break; }
memcpy(c->message+c->messageUsed, data, n); c->messageUsed += n;
c->fragmented = !fin;
if (fin) {
c->message[c->messageUsed] = 0;
net->handler(c, C4F_NET_MESSAGE_EVENT, c->message, c->messageUsed, net->context);
c->messageUsed = 0;
}
} else { c->closing = 1; break; }
size_t taken = head+4+n;
memmove(c->rx, c->rx+taken, c->rxUsed-taken); c->rxUsed -= taken;
}
}
int c4fNetOpen(C4fNet *net, int port, C4fNetHandler handler, void *context)
{
struct sockaddr_in addr;
int one = 1;
memset(net, 0, sizeof(*net)); net->fd = -1;
for (int i = 0; i < C4F_NET_CLIENTS; i++) net->clients[i].fd = -1;
net->handler = handler; net->context = context;
net->fd = socket(AF_INET, SOCK_STREAM, 0);
if (net->fd < 0) return -1;
setsockopt(net->fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
memset(&addr, 0, sizeof(addr));
#ifdef __FreeBSD__
addr.sin_len = sizeof(addr);
#endif
addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_port = htons(port);
if (net->fd >= (int)FD_SETSIZE || fcntl(net->fd, F_SETFL, O_NONBLOCK) ||
bind(net->fd, (struct sockaddr *)&addr, sizeof(addr)) || listen(net->fd, 8)) {
close(net->fd); net->fd = -1; return -1;
}
return 0;
}
void c4fNetPoll(C4fNet *net, int timeoutMs)
{
fd_set rd, wr;
struct timeval timeout = { .tv_sec = 0, .tv_usec = timeoutMs * 1000 };
int maxFd = net->fd, ready;
FD_ZERO(&rd); FD_ZERO(&wr); FD_SET(net->fd, &rd);
for (int i = 0; i < C4F_NET_CLIENTS; i++) {
C4fNetClient *c = &net->clients[i];
if (c->fd < 0) continue;
if (c->closing == 1) { c4fDrop(net, c); continue; }
if (!c->closing && !c->body) FD_SET(c->fd, &rd);
if (c->txSent < c->txUsed || c->body) FD_SET(c->fd, &wr);
if (c->fd > maxFd) maxFd = c->fd;
}
ready = select(maxFd+1, &rd, &wr, NULL, &timeout);
if (ready < 0) return;
if (FD_ISSET(net->fd, &rd)) {
struct sockaddr_in peer;
socklen_t peerSize = sizeof(peer);
int fd = accept(net->fd, (struct sockaddr *)&peer, &peerSize), index;
for (index = 0; index < C4F_NET_CLIENTS && net->clients[index].fd >= 0; index++) {}
if (fd >= 0) {
if (index == C4F_NET_CLIENTS || fd >= (int)FD_SETSIZE || fcntl(fd, F_SETFL, O_NONBLOCK)) close(fd);
else {
int one = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
#ifdef SO_NOSIGPIPE
setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one));
#endif
net->clients[index].fd = fd; net->clients[index].openedMs = c4fTimeMs();
net->clients[index].loopback = (ntohl(peer.sin_addr.s_addr) >> 24) == 127;
}
}
}
for (int i = 0; i < C4F_NET_CLIENTS; i++) {
C4fNetClient *c = &net->clients[i];
if (c->fd < 0) continue;
if (FD_ISSET(c->fd, &rd)) {
ssize_t n = recv(c->fd, c->rx+c->rxUsed, sizeof(c->rx)-c->rxUsed, 0);
if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)) c->closing = 1;
else if (n > 0) {
c->rxUsed += (size_t)n;
if (!c->websocket) c4fHttp(net, c);
if (c->websocket) c4fWs(net, c);
}
}
if (c->closing != 1 && FD_ISSET(c->fd, &wr)) {
const unsigned char *data = c->tx+c->txSent;
size_t left = c->txUsed-c->txSent;
int body = left == 0, flags = 0;
if (body) { data = c->body+c->bodySent; left = c->bodySize-c->bodySent; }
if (left > 16384) left = 16384;
#ifdef MSG_NOSIGNAL
flags = MSG_NOSIGNAL;
#endif
ssize_t n = send(c->fd, data, left, flags);
if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) c->closing = 1;
else if (n > 0) {
if (body) {
c->bodySent += (size_t)n;
if (c->bodySent == c->bodySize) { c->body = NULL; c->closing = 1; }
} else { c->txSent += (size_t)n; if (c->txSent == c->txUsed) c->txSent = c->txUsed = 0; }
}
}
if (!c->websocket && c4fTimeMs()-c->openedMs > 10000) c->closing = 1;
if (c->closing == 1 || (c->closing == 2 && c->txSent == c->txUsed)) c4fDrop(net, c);
}
}
void c4fNetClose(C4fNet *net)
{
for (int i = 0; i < C4F_NET_CLIENTS; i++) if (net->clients[i].fd >= 0) c4fDrop(net, &net->clients[i]);
if (net->fd >= 0) close(net->fd);
net->fd = -1;
}