Skip to content

Commit 52e19b7

Browse files
committed
feat: refactor Socket.IO handling and improve API proxy configuration
- Updated the Socket.IO connection logic to use a unified API proxy path, enhancing maintainability. - Removed deprecated socket handling functions and adjusted related components for improved clarity. - Enhanced the Docker and Nuxt configurations to streamline API and WebSocket traffic management. - Added comments in configuration files to clarify the setup for Socket.IO and reverse-proxy integration.
1 parent 39599ab commit 52e19b7

14 files changed

Lines changed: 181 additions & 187 deletions

File tree

apps/api/.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# SESAME_LOG_LEVEL="info"
2+
# Activer derrière le reverse-proxy Nuxt (proxy /api vers l'API)
3+
# SESAME_TRUST_PROXY=1
24
# SESAME_NAME_QUEUE="sesame"
35
SESAME_JWT_SECRET="zeaezazeaezazaeeazrftrqezfqfqszewfsqddfqsqsqsdqdsqdsqdzsdqzsdqzs"
46
# Docker (make dev) : hostname du conteneur Redis sur le réseau dev

apps/api/src/_common/functions/resolve-client-ip.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ function tcpPeerIp(req: Request): string | null {
4343
return normalizeIp(req.socket?.remoteAddress ?? null);
4444
}
4545

46+
function isLoopbackIp(ip: string | null): boolean {
47+
return ip === '127.0.0.1' || ip === '::1';
48+
}
49+
50+
function isTrustProxyEnabled(): boolean {
51+
return /^(1|true|on|yes)$/i.test(process.env['SESAME_TRUST_PROXY'] || '');
52+
}
53+
4654
/**
4755
* Résout l'IP client réelle derrière CDN / reverse-proxy (Cloudflare, nginx, etc.).
4856
* À utiliser pour l'auth et les audits ; combiner avec `trust proxy` sur Express si besoin.
@@ -53,6 +61,7 @@ function tcpPeerIp(req: Request): string | null {
5361
*/
5462
export function resolveClientIp(req: Request): string | null {
5563
const peerIp = tcpPeerIp(req);
64+
const trustedLocalProxy = isTrustProxyEnabled() && isLoopbackIp(peerIp);
5665
const orderedHeaders = ['cf-connecting-ip', 'true-client-ip', 'x-forwarded-for', 'x-real-ip'] as const;
5766

5867
for (const name of orderedHeaders) {
@@ -63,7 +72,8 @@ export function resolveClientIp(req: Request): string | null {
6372
continue;
6473
}
6574
// Ne pas prendre un « forwarded » qui ne fait que recopier le pair TCP (pas de chaîne utile).
66-
if (peerIp && ip === peerIp) {
75+
// Exception : derrière le proxy Nuxt local, la chaîne XFF peut être « client, 127.0.0.1 ».
76+
if (peerIp && ip === peerIp && !(trustedLocalProxy && name === 'x-forwarded-for' && raw?.includes(','))) {
6777
continue;
6878
}
6979
return ip;

apps/api/tests/unit/_common/functions/resolve-client-ip.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,34 @@ describe('resolveClientIp', () => {
8686
expect(p.tcpPeerNormalized).toBe('140.82.121.5');
8787
});
8888

89+
it('returns X-Real-IP when tcp peer is loopback and trust proxy is enabled', () => {
90+
const previous = process.env.SESAME_TRUST_PROXY;
91+
process.env.SESAME_TRUST_PROXY = '1';
92+
try {
93+
const req = makeReq({
94+
headers: { 'x-real-ip': '192.168.1.190' },
95+
socket: { remoteAddress: '127.0.0.1' } as any,
96+
});
97+
expect(resolveClientIp(req)).toBe('192.168.1.190');
98+
} finally {
99+
process.env.SESAME_TRUST_PROXY = previous;
100+
}
101+
});
102+
103+
it('returns first X-Forwarded-For hop behind loopback proxy when trust proxy is enabled', () => {
104+
const previous = process.env.SESAME_TRUST_PROXY;
105+
process.env.SESAME_TRUST_PROXY = '1';
106+
try {
107+
const req = makeReq({
108+
headers: { 'x-forwarded-for': '192.168.65.1, 127.0.0.1' },
109+
socket: { remoteAddress: '127.0.0.1' } as any,
110+
});
111+
expect(resolveClientIp(req)).toBe('192.168.65.1');
112+
} finally {
113+
process.env.SESAME_TRUST_PROXY = previous;
114+
}
115+
});
116+
89117
it('keeps raw peer fields in debug payload', () => {
90118
const req = makeReq({
91119
headers: { host: 'host.docker.internal:4002' },

apps/web/nuxt.config.ts

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,15 @@ import setupApp from './src/server/extension.setup'
77
import { loadingBarHijackFilter } from './src/composables/useLoadingBarHijackFilter'
88

99
const SESAME_APP_API_URL = process.env.SESAME_APP_API_URL || 'http://127.0.0.1:4000'
10-
const SESAME_APP_API_URL_PARSED = new URL(SESAME_APP_API_URL)
11-
/** URL API exposée au navigateur (WebSocket). Ex. http://mactacx:4002 (Docker) ou :4000 (API native). */
12-
const SESAME_APP_PUBLIC_API_URL = process.env.SESAME_APP_PUBLIC_API_URL || ''
13-
/** Port API vu depuis le navigateur (Docker : 4002 mappé, interne API : 4000). */
14-
const SESAME_APP_PUBLIC_API_PORT = process.env.SESAME_APP_PUBLIC_API_PORT || ''
1510
const SESAME_ALLOWED_HOSTS = process.env.SESAME_ALLOWED_HOSTS ? process.env.SESAME_ALLOWED_HOSTS.split(',') : []
16-
const SOCKET_IO_PROXY_TARGET = SESAME_APP_API_URL.replace(/\/$/, '')
11+
const API_PROXY_TARGET = SESAME_APP_API_URL.replace(/\/$/, '')
1712
const IS_DEV = process.env.NODE_ENV === 'development'
1813

1914
if (SESAME_ALLOWED_HOSTS.length === 0 && !/localhost/.test(SESAME_APP_API_URL) && IS_DEV) {
2015
SESAME_ALLOWED_HOSTS.push(new URL(SESAME_APP_API_URL).hostname)
2116
}
2217

2318
consola.info(`[Nuxt] SESAME_APP_API_URL: ${SESAME_APP_API_URL}`)
24-
consola.info(`[Nuxt] Socket.IO public port: ${SESAME_APP_PUBLIC_API_PORT || SESAME_APP_API_URL_PARSED.port || '4000'}`)
2519
consola.info(`[Nuxt] SESAME_ALLOWED_HOSTS: ${SESAME_ALLOWED_HOSTS}`)
2620

2721
let SESAME_APP_DARK_MODE: 'auto' | boolean = false
@@ -84,17 +78,14 @@ export default defineNuxtConfig({
8478
runtimeConfig: {
8579
public: {
8680
release: process.env.npm_package_name + '@' + process.env.npm_package_version,
87-
socketApiUrl: SESAME_APP_PUBLIC_API_URL,
88-
socketApiPort: SESAME_APP_API_URL_PARSED.port || (SESAME_APP_API_URL_PARSED.protocol === 'https:' ? '443' : '4000'),
89-
socketPublicApiPort: SESAME_APP_PUBLIC_API_PORT || SESAME_APP_API_URL_PARSED.port || (SESAME_APP_API_URL_PARSED.protocol === 'https:' ? '443' : '4000'),
90-
socketApiProtocol: SESAME_APP_API_URL_PARSED.protocol,
9181
sentry: {
9282
dsn: process.env.SESAME_SENTRY_DSN,
9383
},
9484
},
9585
},
9686
modules: [
9787
'@sentry/nuxt/module',
88+
'@nuxt-alt/proxy',
9889
'@nuxt-alt/auth',
9990
'@nuxt-alt/http',
10091
'@pinia/nuxt',
@@ -105,6 +96,21 @@ export default defineNuxtConfig({
10596
'nuxt-monaco-editor',
10697
...setupApp(),
10798
],
99+
proxy: {
100+
debug: IS_DEV,
101+
experimental: {
102+
listener: true,
103+
},
104+
proxies: {
105+
'/api': {
106+
target: API_PROXY_TARGET,
107+
changeOrigin: true,
108+
ws: true,
109+
xfwd: true,
110+
rewrite: (path: string) => path.replace(/^\/api/, ''),
111+
},
112+
},
113+
},
108114
sentry: {
109115
autoInjectServerSentry: "top-level-import",
110116
},
@@ -187,7 +193,7 @@ export default defineNuxtConfig({
187193
color: 'primary',
188194
size: '3px',
189195
position: 'top',
190-
/** Exclut socket.io (long-polling) et le polling de synchro / daemon. */
196+
/** Exclut Socket.IO (`/api/socket.io`) et le polling de synchro / daemon. */
191197
hijackFilter: loadingBarHijackFilter,
192198
},
193199
},
@@ -206,11 +212,13 @@ export default defineNuxtConfig({
206212
vite: {
207213
server: {
208214
allowedHosts: ['localhost', ...SESAME_ALLOWED_HOSTS],
215+
// En dev, le navigateur parle à Vite : seul Vite peut faire l'upgrade WS (Nitro proxy.web → Invalid frame header).
209216
proxy: {
210-
'/socket.io': {
211-
target: SOCKET_IO_PROXY_TARGET,
217+
'/api/socket.io': {
218+
target: API_PROXY_TARGET,
212219
changeOrigin: true,
213220
ws: true,
221+
rewrite: (path: string) => path.replace(/^\/api/, ''),
214222
},
215223
},
216224
},
@@ -262,17 +270,9 @@ export default defineNuxtConfig({
262270
typescriptBundlerResolution: true,
263271
},
264272
nitro: {
265-
devProxy: {
266-
'/socket.io': {
267-
target: SOCKET_IO_PROXY_TARGET,
268-
changeOrigin: true,
269-
ws: true,
270-
},
271-
},
272-
routeRules: {
273-
'/api/**': {
274-
proxy: `${SESAME_APP_API_URL}/**`,
275-
},
273+
experimental: {
274+
/** Requis par @nuxt-alt/proxy pour ne pas intercepter les upgrades WS. */
275+
websocket: false,
276276
},
277277
},
278278
experimental: {

apps/web/src/composables/useLoadingBarHijackFilter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** Requêtes en arrière-plan : ne pas déclencher la barre Quasar (XHR hijack). */
22
const BACKGROUND_URL_PARTS = [
3-
'/socket.io',
3+
'/api/socket.io',
44
'/core/backends/sync-progress',
55
]
66

apps/web/src/composables/useSocketApiOrigin.ts

Lines changed: 0 additions & 87 deletions
This file was deleted.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { ManagerOptions, SocketOptions } from 'socket.io-client'
2+
3+
/** Même préfixe que le proxy `/api/**` → API (cf. `nuxt.config.ts` routeRules). */
4+
export const SOCKET_IO_PATH = '/api/socket.io'
5+
6+
/** Polling d'abord en dev (proxy Nitro HTTP) ; upgrade WS via Vite. Prod : WS en priorité. */
7+
export function socketIoTransports(): ('websocket' | 'polling')[] {
8+
return import.meta.dev ? ['polling', 'websocket'] : ['websocket', 'polling']
9+
}
10+
11+
export function buildSocketIoClientOptions(auth: { id: string; key: string }): Partial<ManagerOptions & SocketOptions> {
12+
return {
13+
path: SOCKET_IO_PATH,
14+
query: { id: String(auth.id), key: String(auth.key) },
15+
transports: socketIoTransports(),
16+
reconnectionAttempts: 10,
17+
}
18+
}

apps/web/src/layouts/default.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { IdentityState } from '~/constants/enums'
2424
import { useIdentityStateStore } from '~/stores/identityState'
2525
import { loadingBarDefaults } from '~/composables/useLoadingBarHijackFilter'
2626
import { attachSocketIoDebug } from '~/composables/useSocketIoDebug'
27-
import { buildSocketIoClientOptions, resolveSocketApiOrigin } from '~/composables/useSocketApiOrigin'
27+
import { buildSocketIoClientOptions } from '~/composables/useSocketIoClient'
2828
import { io, type Socket } from 'socket.io-client'
2929
3030
export default defineNuxtComponent({
@@ -159,7 +159,7 @@ export default defineNuxtComponent({
159159
160160
this.disconnectBackendsSocket()
161161
162-
this.socket = io(`${resolveSocketApiOrigin()}/core/backends`, buildSocketIoClientOptions({ id, key }))
162+
this.socket = io('/core/backends', buildSocketIoClientOptions({ id, key }))
163163
attachSocketIoDebug(this.socket, '/core/backends')
164164
this.socket.on('connect', () => {
165165
Object.assign(this.daemonStatus, { checking: true })

apps/web/src/pages/settings/cron.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@
220220
import type { LocationQueryValue } from 'vue-router'
221221
import { reactive, ref } from 'vue'
222222
import { attachSocketIoDebug } from '~/composables/useSocketIoDebug'
223-
import { buildSocketIoClientOptions, resolveSocketApiOrigin } from '~/composables/useSocketApiOrigin'
223+
import { buildSocketIoClientOptions } from '~/composables/useSocketIoClient'
224224
import { io, type Socket } from 'socket.io-client'
225225
import { NewTargetId } from '~/constants/variables'
226226
@@ -597,7 +597,7 @@ export default defineNuxtComponent({
597597
this.logsFollowTail = true
598598
this.logsLoading = true
599599
600-
this.logsSocket = io(`${resolveSocketApiOrigin()}/core/cron`, buildSocketIoClientOptions({ id, key }))
600+
this.logsSocket = io('/core/cron', buildSocketIoClientOptions({ id, key }))
601601
attachSocketIoDebug(this.logsSocket, '/core/cron')
602602
603603
this.logsSocket.on('connect', () => {

apps/web/src/server/middleware/forward-client-ip.ts

Lines changed: 0 additions & 41 deletions
This file was deleted.

0 commit comments

Comments
 (0)