From b87f4988e1322990a646fcd99acfb324d4d8834a Mon Sep 17 00:00:00 2001 From: Daniel Salazar Date: Thu, 19 Mar 2026 14:32:20 -0700 Subject: [PATCH] fix: socket io sockets (#2696) --- package-lock.json | 2 +- src/backend/src/helpers.js | 9 ++- .../src/modules/web/SocketioService.js | 4 +- .../src/modules/web/WebServerService.js | 73 +++++++++++-------- src/backend/src/routers/_default.js | 3 +- src/backend/src/services/WSPushService.js | 39 +++++----- src/gui/src/helpers/download.js | 1 + src/puter-js/package-lock.json | 4 +- src/puter-js/package.json | 2 +- src/puter-js/src/lib/utils.js | 31 +++++--- .../modules/FileSystem/operations/upload.js | 1 + 11 files changed, 98 insertions(+), 71 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6d46b3d5b..55ec9f68d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24422,7 +24422,7 @@ }, "src/puter-js": { "name": "@heyputer/puter.js", - "version": "2.2.12", + "version": "2.2.14", "license": "Apache-2.0", "dependencies": { "@heyputer/kv.js": "^0.2.1", diff --git a/src/backend/src/helpers.js b/src/backend/src/helpers.js index ede5d814b..037e7ca28 100644 --- a/src/backend/src/helpers.js +++ b/src/backend/src/helpers.js @@ -1546,7 +1546,7 @@ export function subdomain (req) { return req.hostname.slice(0, -1 * (config.domain.length + 1)); } -export async function jwt_auth (req) { +export async function jwt_auth (req, authService) { let token; // HTTML Auth header if ( req.header && req.header('Authorization') ) @@ -1583,8 +1583,11 @@ export async function jwt_auth (req) { } try { - const svc_auth = Context.get('services').get('auth'); - const actor = await svc_auth.authenticate_from_token(token); + if ( ! authService ) { + throw new Error('jwt_auth requires authService'); + } + + const actor = await authService.authenticate_from_token(token); if ( !actor.type?.constructor?.name === 'UserActorType' ) { throw ({ diff --git a/src/backend/src/modules/web/SocketioService.js b/src/backend/src/modules/web/SocketioService.js index de0c954b1..667f995af 100644 --- a/src/backend/src/modules/web/SocketioService.js +++ b/src/backend/src/modules/web/SocketioService.js @@ -66,9 +66,7 @@ class SocketioService extends BaseService { if ( socket_specifier.room ) { this.io.to(socket_specifier.room).emit(key, data); } else if ( socket_specifier.socket ) { - const io = this.io.sockets.sockets.get(socket_specifier.socket); - if ( ! io ) continue; - io.emit(key, data); + this.io.to(socket_specifier.socket).emit(key, data); } } } diff --git a/src/backend/src/modules/web/WebServerService.js b/src/backend/src/modules/web/WebServerService.js index 70769efb1..9676cdf01 100644 --- a/src/backend/src/modules/web/WebServerService.js +++ b/src/backend/src/modules/web/WebServerService.js @@ -260,27 +260,36 @@ class WebServerService extends BaseService { // TODO: ^ Replace above line with the following code: await this.services.emit('install.socketio', { server }); const socketio = this.services.get('socketio').io; + const authService = this.services.get('auth'); // Socket.io middleware for authentication socketio.use(async (socket, next) => { - if ( socket.handshake.auth.auth_token ) { - try { - let auth_res = await jwt_auth(socket); - // successful auth - socket.actor = auth_res.actor; - socket.user = auth_res.user; - socket.token = auth_res.token; - // join user room - socket.join(socket.user.id); + const authToken = socket.handshake?.auth?.auth_token; + if ( ! authToken ) { + next(new Error('socket auth token missing')); + return; + } - // setTimeout 0 is needed because we need to send - // the notifications after this handler is done - // setTimeout(() => { - // }, 1000); - next(); - } catch (e) { - console.warn('socket auth err', e); - } + try { + const authRes = await jwt_auth(socket, authService); + // successful auth + socket.actor = authRes.actor; + socket.user = authRes.user; + socket.token = authRes.token; + // join user room + socket.join(socket.user.id); + + // setTimeout 0 is needed because we need to send + // the notifications after this handler is done + // setTimeout(() => { + // }, 1000); + next(); + } catch ( error ) { + console.warn('socket auth err', error); + const authError = error instanceof Error + ? error + : new Error('socket auth failed'); + next(authError); } }); @@ -674,6 +683,16 @@ class WebServerService extends BaseService { app.use(function (req, res, next) { const origin = req.headers.origin; + const subdomain = req.subdomains[req.subdomains.length - 1]; + const isApiOrDavRequest = + config.experimental_no_subdomain || + subdomain === 'api' || + subdomain === 'dav'; + const isCrossOriginAuthRoute = + req.path === '/signup' || + req.path === '/login' || + req.path.startsWith('/extensions/') || + req.path.startsWith('/auth/oidc'); const is_site = hostMatchesDomain(req.hostname, config.static_hosting_domain) || @@ -692,16 +711,16 @@ class WebServerService extends BaseService { req.co_isolation_enabled ; - if ( req.path === '/signup' || req.path === '/login' || req.path.startsWith('/extensions/') || req.path.startsWith('/auth/oidc') ) { + if ( isCrossOriginAuthRoute || isApiOrDavRequest ) { res.setHeader('Access-Control-Allow-Origin', origin ?? '*'); + if ( origin ) { + res.vary('Origin'); + } } - // Website(s) to allow to connect - if ( - config.experimental_no_subdomain || - req.subdomains[req.subdomains.length - 1] === 'api' || - req.subdomains[req.subdomains.length - 1] === 'dav' - ) { - res.setHeader('Access-Control-Allow-Origin', origin ?? '*'); + + // Allow browser credentials on API/DAV cross-origin requests. + if ( isApiOrDavRequest && origin ) { + res.setHeader('Access-Control-Allow-Credentials', 'true'); } // Request methods to allow @@ -715,10 +734,6 @@ class WebServerService extends BaseService { // Request headers to allow res.header('Access-Control-Allow-Headers', allowed_headers.join(', ')); - // Set to true if you need the website to include cookies in the requests sent - // to the API (e.g. in case you use sessions) - // res.setHeader('Access-Control-Allow-Credentials', true); - // Needed for SharedArrayBuffer // NOTE: This is put behind a configuration flag because we // need some experimentation to ensure the interface diff --git a/src/backend/src/routers/_default.js b/src/backend/src/routers/_default.js index c1ce46b2e..7295d6581 100644 --- a/src/backend/src/routers/_default.js +++ b/src/backend/src/routers/_default.js @@ -113,6 +113,7 @@ router.all('*', async function (req, res, next) { } const db = Context.get('services').get('database').get(DB_READ, 'default'); + const authService = Context.get('services').get('auth'); // -------------------------------------- // POST to login/signup/logout @@ -137,7 +138,7 @@ router.all('*', async function (req, res, next) { let authed = false; try { try { - auth_user = await jwt_auth(req); + auth_user = await jwt_auth(req, authService); auth_user = auth_user.user; authed = true; } catch (e) { diff --git a/src/backend/src/services/WSPushService.js b/src/backend/src/services/WSPushService.js index af9a95c44..86f4c2703 100644 --- a/src/backend/src/services/WSPushService.js +++ b/src/backend/src/services/WSPushService.js @@ -34,14 +34,22 @@ class WSPushService extends BaseService { this.svc_event.on('fs.write.*', this._on_fs_update.bind(this)); this.svc_event.on('fs.move.*', this._on_fs_move.bind(this)); this.svc_event.on('fs.pending.*', this._on_fs_pending.bind(this)); - this.svc_event.on('fs.storage.upload-progress', - this._on_upload_progress.bind(this)); - this.svc_event.on('fs.storage.progress.*', - this._on_upload_progress.bind(this)); - this.svc_event.on('puter-exec.submission.done', - this._on_submission_done.bind(this)); - this.svc_event.on('outer.gui.*', - this._on_outer_gui.bind(this)); + this.svc_event.on( + 'fs.storage.upload-progress', + this._on_upload_progress.bind(this), + ); + this.svc_event.on( + 'fs.storage.progress.*', + this._on_upload_progress.bind(this), + ); + this.svc_event.on( + 'puter-exec.submission.done', + this._on_submission_done.bind(this), + ); + this.svc_event.on( + 'outer.gui.*', + this._on_outer_gui.bind(this), + ); } async _on_fs_create (key, data) { @@ -251,16 +259,12 @@ class WSPushService extends BaseService { const { socket_id } = metadata; if ( ! socket_id ) { - this.log.warn('missing socket id', { metadata }); - } - - this.log.info(`socket id: ${ socket_id}`); - - const svc_socketio = context.get('services').get('socketio'); - if ( ! svc_socketio.has({ socket: socket_id }) ) { + console.warn('missing socket id', { metadata }); return; } + const svc_socketio = context.get('services').get('socketio'); + const ws_event_name = metadata.call_it_download ? 'download.progress' : 'upload.progress'; @@ -311,9 +315,6 @@ class WSPushService extends BaseService { const svc_socketio = this.services.get('socketio'); for ( const user_id of user_id_list ) { - if ( ! svc_socketio.has({ room: user_id }) ) { - continue; - } svc_socketio.send({ room: user_id }, key, response); this.svc_event.emit(`sent-to-user.${key}`, { user_id, @@ -340,7 +341,7 @@ class WSPushService extends BaseService { const kvStore = Context.get('services').get('puter-kvstore'); await kvStore.set({ key: key, value: ts }); } catch ( error ) { - this.log.error('Failed to update user timestamp in kvstore', { user_id, error: error.message }); + console.error('Failed to update user timestamp in kvstore', { user_id, error: error.message }); } } diff --git a/src/gui/src/helpers/download.js b/src/gui/src/helpers/download.js index 2322e3797..ada557582 100644 --- a/src/gui/src/helpers/download.js +++ b/src/gui/src/helpers/download.js @@ -49,6 +49,7 @@ const download = function (options) { let xhr = new XMLHttpRequest(); xhr.open('post', (`${window.api_origin }/download`), true); + xhr.withCredentials = true; xhr.setRequestHeader('Authorization', `Bearer ${ window.auth_token}`); xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); diff --git a/src/puter-js/package-lock.json b/src/puter-js/package-lock.json index 3ef83eabd..97841416f 100644 --- a/src/puter-js/package-lock.json +++ b/src/puter-js/package-lock.json @@ -1,12 +1,12 @@ { "name": "puter", - "version": "2.2.12", + "version": "2.2.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "puter", - "version": "2.2.12", + "version": "2.2.14", "license": "Apache-2.0", "dependencies": { "@heyputer/kv.js": "^0.1.92", diff --git a/src/puter-js/package.json b/src/puter-js/package.json index ba8e7542a..01edd7424 100644 --- a/src/puter-js/package.json +++ b/src/puter-js/package.json @@ -1,6 +1,6 @@ { "name": "@heyputer/puter.js", - "version": "2.2.12", + "version": "2.2.14", "description": "Puter.js - A JavaScript library for interacting with Puter services.", "homepage": "https://developer.puter.com", "main": "src/index.js", diff --git a/src/puter-js/src/lib/utils.js b/src/puter-js/src/lib/utils.js index 2f62b4cce..3ee6a0f0a 100644 --- a/src/puter-js/src/lib/utils.js +++ b/src/puter-js/src/lib/utils.js @@ -92,6 +92,7 @@ const createDeferred = () => { function initXhr (endpoint, APIOrigin, authToken, method = 'post', contentType = 'text/plain;actually=json', responseType = undefined) { const xhr = new XMLHttpRequest(); xhr.open(method, APIOrigin + endpoint, true); + xhr.withCredentials = true; if ( authToken ) { xhr.setRequestHeader('Authorization', `Bearer ${ authToken}`); @@ -280,16 +281,18 @@ function make_driver_method (arg_defs, driverInterface, driverName, driverMethod async function driverCall (options, driverInterface, driverName, driverMethod, driverArgs, settings) { const deferred = createDeferred(); - driverCall_(options, - deferred.resolve, - deferred.reject, - driverInterface, - driverName, - driverMethod, - driverArgs, - undefined, - undefined, - settings); + driverCall_( + options, + deferred.resolve, + deferred.reject, + driverInterface, + driverName, + driverMethod, + driverArgs, + undefined, + undefined, + settings, + ); return await deferred.promise; } @@ -297,8 +300,12 @@ async function driverCall (options, driverInterface, driverName, driverMethod, d // This function encapsulates the logic for sending a driver call request async function driverCall_ ( options = {}, - resolve_func, reject_func, - driverInterface, driverName, driverMethod, driverArgs, + resolve_func, + reject_func, + driverInterface, + driverName, + driverMethod, + driverArgs, method, contentType = 'text/plain;actually=json', settings = {}, diff --git a/src/puter-js/src/modules/FileSystem/operations/upload.js b/src/puter-js/src/modules/FileSystem/operations/upload.js index bfbb9d216..fb462de13 100644 --- a/src/puter-js/src/modules/FileSystem/operations/upload.js +++ b/src/puter-js/src/modules/FileSystem/operations/upload.js @@ -457,6 +457,7 @@ const upload = async function (items, dirPath, options = {}) { // open request to server xhr.open('post', (`${this.APIOrigin }/batch`), true); + xhr.withCredentials = true; // set auth header xhr.setRequestHeader('Authorization', `Bearer ${ this.authToken}`);