diff --git a/src/main/features/core/remote/index.ts b/src/main/features/core/remote/index.ts index 4a43e81e..2c2b87c0 100644 --- a/src/main/features/core/remote/index.ts +++ b/src/main/features/core/remote/index.ts @@ -34,6 +34,7 @@ interface MimeType { interface StatefulWebSocket extends WebSocket { alive: boolean; + auth: boolean; } let server: Server | undefined; @@ -52,7 +53,7 @@ type SendData = ServerEvent & { function send({ client, event, data }: SendData): void { if (client.readyState === WebSocket.OPEN) { - if (client.alive) { + if (client.alive && client.auth) { client.send(JSON.stringify({ data, event })); } } @@ -320,12 +321,13 @@ const enableServer = (config: RemoteConfig): Promise => { wsServer.on('connection', (ws) => { let authFail: number | undefined; + ws.alive = true; if (!settings.username && !settings.password) { - ws.alive = true; + ws.auth = true; } else { authFail = setTimeout(() => { - if (!ws.alive) { + if (!ws.auth) { ws.close(); } }, 10000) as unknown as number; @@ -334,30 +336,29 @@ const enableServer = (config: RemoteConfig): Promise => { ws.on('error', console.error); ws.on('message', (data) => { - if (!ws.alive) { - try { - const auth = data.toString().split(' ')[1]; - const [login, password] = Buffer.from(auth, 'base64') - .toString() - .split(':'); - - if (login === settings.username && password === settings.password) { - ws.alive = true; - } else { - ws.close(); - } - - clearTimeout(authFail); - } catch (e) { - console.error(e); - } - return; - } - try { const json = JSON.parse(data.toString()) as ClientEvent; const event = json.event; + if (!ws.auth) { + if (event === 'authenticate') { + const auth = json.header.split(' ')[1]; + const [login, password] = Buffer.from(auth, 'base64') + .toString() + .split(':'); + + if (login === settings.username && password === settings.password) { + ws.auth = true; + } else { + ws.close(); + } + + clearTimeout(authFail); + } else { + return; + } + } + switch (event) { case 'pause': { getMainWindow()?.webContents.send('renderer-player-pause'); diff --git a/src/remote/store/index.ts b/src/remote/store/index.ts index 01ee571f..2abbb753 100644 --- a/src/remote/store/index.ts +++ b/src/remote/store/index.ts @@ -87,7 +87,7 @@ export const useRemoteStore = create()( devtools( immer((set, get) => ({ actions: { - reconnect: () => { + reconnect: async () => { const existing = get().socket; if (existing) { @@ -99,91 +99,102 @@ export const useRemoteStore = create()( existing.close(4001); } } - set(async (state) => { - try { - const credentials = await fetch('/credentials'); - const authHeader = await credentials.text(); - const socket = new WebSocket( + let authHeader: string | undefined; + + try { + const credentials = await fetch('/credentials'); + authHeader = await credentials.text(); + } catch (error) { + console.error('Failed to get credentials'); + } + + set((state) => { + const socket = new WebSocket( + // eslint-disable-next-line no-restricted-globals + location.href.replace('http', 'ws'), + ) as StatefulWebSocket; + + socket.natural = false; + + socket.addEventListener('message', (message) => { + const { event, data } = JSON.parse(message.data) as ServerEvent; + + switch (event) { + case 'error': { + toast.error({ message: data, title: 'Socket error' }); + break; + } + case 'favorite': { + set((state) => { + if (state.info.song?.id === data.id) { + state.info.song.userFavorite = data.favorite; + } + }); + break; + } + case 'proxy': { + set((state) => { + if (state.info.song) { + state.info.song.imageUrl = `data:image/jpeg;base64,${data}`; + } + }); + break; + } + case 'rating': { + set((state) => { + if (state.info.song?.id === data.id) { + state.info.song.userRating = data.rating; + } + }); + break; + } + case 'song': { + set((nested) => { + nested.info = { ...nested.info, ...data }; + }); + } + } + }); + + socket.addEventListener('open', () => { + if (authHeader) { + socket.send( + JSON.stringify({ + event: 'authenticate', + header: authHeader, + }), + ); + } + set({ connected: true }); + }); + + socket.addEventListener('close', (reason) => { + if (reason.code === 4002 || reason.code === 4003) { // eslint-disable-next-line no-restricted-globals - location.href.replace('http', 'ws'), - ) as StatefulWebSocket; + location.reload(); + } else if (reason.code === 4000) { + toast.warn({ + message: 'Feishin remote server is down', + title: 'Connection closed', + }); + } else if (reason.code !== 4001 && !socket.natural) { + toast.error({ + message: 'Socket closed for unexpected reason', + title: 'Connection closed', + }); + } - socket.natural = false; + if (!socket.natural) { + set({ connected: false, info: {} }); + } + }); - socket.addEventListener('message', (message) => { - const { event, data } = JSON.parse(message.data) as ServerEvent; - - switch (event) { - case 'error': { - toast.error({ message: data, title: 'Socket error' }); - break; - } - case 'favorite': { - set((state) => { - if (state.info.song?.id === data.id) { - state.info.song.userFavorite = data.favorite; - } - }); - break; - } - case 'proxy': { - set((state) => { - if (state.info.song) { - state.info.song.imageUrl = `data:image/jpeg;base64,${data}`; - } - }); - break; - } - case 'rating': { - set((state) => { - if (state.info.song?.id === data.id) { - state.info.song.userRating = data.rating; - } - }); - break; - } - case 'song': { - set((nested) => { - nested.info = { ...nested.info, ...data }; - }); - } - } - }); - - socket.addEventListener('open', () => { - socket.send(authHeader); - set({ connected: true }); - }); - - socket.addEventListener('close', (reason) => { - if (reason.code === 4002 || reason.code === 4003) { - // eslint-disable-next-line no-restricted-globals - location.reload(); - } else if (reason.code === 4000) { - toast.warn({ - message: 'Feishin remote server is down', - title: 'Connection closed', - }); - } else if (reason.code !== 4001 && !socket.natural) { - toast.error({ - message: 'Socket closed for unexpected reason', - title: 'Connection closed', - }); - } - - if (!socket.natural) { - set({ connected: false, info: {} }); - } - }); - - state.socket = socket; - } catch (err) { - console.error(err); - } + state.socket = socket; }); }, send: (data: ClientEvent) => { + console.log(data, get().socket); get().socket?.send(JSON.stringify(data)); }, toggleIsDark: () => { diff --git a/src/remote/types.ts b/src/remote/types.ts index f5a648f4..0ef32ed9 100644 --- a/src/remote/types.ts +++ b/src/remote/types.ts @@ -53,4 +53,14 @@ export interface ClientVolume { volume: number; } -export type ClientEvent = ClientSimpleEvent | ClientFavorite | ClientRating | ClientVolume; +export interface ClientAuth { + event: 'authenticate'; + header: string; +} + +export type ClientEvent = + | ClientAuth + | ClientSimpleEvent + | ClientFavorite + | ClientRating + | ClientVolume;