Files
CyberMask367 74ab227d86 Simplify updates and harden websocket drains
This removes the old signed manifest staging flow and makes upgrades check-only notifications, while deleting dead manifest/release-key code and switching default art URLs to the GitHub raw asset pack. It also fixes oversized Discord READY handling by streaming envelope extraction instead of buffering giant frames, avoids repeated reconnect notifications, and updates host tests/build scripts to reflect the new no-install updater path.
2026-10-10 03:25:25 +01:00

551 lines
24 KiB
C

/* 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. */
{
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;
}
/* ws_skip_plan() lives in ws_skip.c and the envelope scraper in ws_env.c, so
* the host tests can compile both without the socket/TLS half of this file. */
#include "ws_skip.c"
#include "ws_env.c"
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. Every discarded byte is fed to the
* envelope scraper first, so op/s/t survive without buffering. */
size_t have = w->rlen - w->rpos;
size_t take = have < (size_t)w->skip_left ? have : (size_t)w->skip_left;
if(take && w->env_active)
ws_env_feed(&w->env, w->rbuf + w->rpos, take);
w->skip_left -= take; w->rpos += take;
if(w->rpos >= w->rlen){ w->rpos=0; w->rlen=0; }
/* Hand back the envelope as soon as op/s/t are known instead of
* after the whole body. A 5.8 MB READY took longer to stream than
* the 20 s identify deadline, so waiting gave
* "no READY after identify (timeout)" even though the frame was
* arriving perfectly. The remainder keeps draining on later
* calls: skip_left and env live in the ws_t, so the next
* ws_recv_frame() resumes exactly where this one stopped. */
if(w->env_active && !w->env_reported && ws_env_complete(&w->env)){
size_t n = ws_env_render(&w->env, buf, cap);
if(n){
w->env_reported = 1;
if(opcode_out)*opcode_out = w->skip_op;
if(fin_out)*fin_out = w->skip_fin;
log_msg("ws: envelope reported early, %lluB still draining",
(unsigned long long)w->skip_left);
return (int)n;
}
}
if(w->skip_left==0){
if(opcode_out)*opcode_out=w->skip_op;
if(fin_out)*fin_out=w->skip_fin;
/* Drain finished. If the envelope never got reported, give it
* one last chance; otherwise this frame was already seen. */
if(w->env_active){
if(w->env_reported){
/* Already delivered this frame's envelope; only the
* tail needed discarding. Report "nothing yet" rather
* than -3, which callers log as a lost frame. */
w->env_active = 0; w->env_reported = 0;
return 0;
}
/* No early report happened, so say what was actually
* captured. Without this the console only shows a bare
* timeout and the cause is invisible. */
log_msg("ws: drain end, envelope incomplete "
"(op=%d %s s=%d %s t=%d %s win=%zu)",
w->env.have_op, w->env.op,
w->env.have_s, w->env.s,
w->env.have_t, w->env.t, w->env.winlen);
size_t n = ws_env_render(&w->env, buf, cap);
w->env_active = 0; w->env_reported = 0;
if(n) return (int)n;
}
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;
}
/* Oversized: never buffer. Start the drain and scrape op/s/t as the
* bytes go past, so a 12 MB READY costs a fixed 2 KB window.
* Buffering it used to force rbuf to double to 16 MB, which
* exhausted console memory across a long session (2026-10-09).
* Size checked BEFORE arithmetic: hdr+plen could overflow
* uint64 and smuggle a tiny "total" past the cap. */
if(plen > WS_BODY_MAX){
log_msg("ws: oversized frame op=%d fin=%d plen=%llu (streamed, body cap %u)",
b[0]&0x0f, (b[0]&0x80)!=0, (unsigned long long)plen, WS_BODY_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;
if(payload_here) ws_env_feed(&w->env, w->rbuf + w->rpos, payload_here);
/* skip_left counts EVERY payload byte still to consume,
* including the ones already sitting in rbuf. Passing
* payload_here here was a double-count: ws_skip_plan()
* subtracted them, then the drain below consumed them
* again, so the drain ended early and the next parse
* read mid-payload as a frame header (observed 2026-10-09
* as op=10 plen=116 b1=0x3a b2=0x74 -- ':' and 't' from
* the middle of READY's "d" object). */
ws_skip_plan(plen, 0, &w->skip_left);
w->skip_op = wire_op;
w->skip_fin = fin;
w->env_active = 1;
w->env_reported = 0;
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 {
/* Once per connection. This used to log on every
* loop pass and buried the actual cause under 20
* identical lines. */
if(!w->grow_warned){
w->grow_warned = 1;
log_msg("ws: rbuf grow fail (want %zu, have %zu); "
"console likely out of memory", ncap, w->rcap);
}
}
}
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){
/* A full buffer during a drain is NORMAL, not a fault: an
* oversized frame is by definition larger than the buffer, so
* the drain can only make progress by discarding what is
* buffered. Bailing out here wiped the buffer and returned -2
* with skip_left outstanding, which left the socket mid-frame
* and made the next parse read JSON payload as a frame header
* (observed: op=2 fin=0 plen=125 b1=0x22 -- a quote byte).
* Consume against skip_left instead; the loop below refills
* from TLS, so no headroom is actually needed. */
if(w->skip_left > 0){
size_t take = (size_t)w->skip_left;
if(take > w->rlen) take = w->rlen;
if(take && w->env_active)
ws_env_feed(&w->env, w->rbuf + w->rpos, take);
w->skip_left -= take;
w->rpos += take;
if(w->rpos >= w->rlen){ w->rpos=0; w->rlen=0; }
if(w->env_active && !w->env_reported && ws_env_complete(&w->env)){
size_t n = ws_env_render(&w->env, buf, cap);
if(n){
w->env_reported = 1;
if(opcode_out)*opcode_out = w->skip_op;
if(fin_out)*fin_out = w->skip_fin;
return (int)n;
}
}
if(w->skip_left == 0){
if(opcode_out)*opcode_out = w->skip_op;
if(fin_out)*fin_out = w->skip_fin;
if(w->env_active){
if(w->env_reported){
w->env_active = 0; w->env_reported = 0;
return 0;
}
size_t n = ws_env_render(&w->env, buf, cap);
w->env_active = 0; w->env_reported = 0;
if(n) return (int)n;
}
return -3;
}
continue;
}
/* full with no parseable frame and no drain: 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;
}