diff --git a/webclient/src/api/response/SessionResponseImpl.ts b/webclient/src/api/response/SessionResponseImpl.ts index ade27a36f..272079889 100644 --- a/webclient/src/api/response/SessionResponseImpl.ts +++ b/webclient/src/api/response/SessionResponseImpl.ts @@ -30,8 +30,8 @@ export class SessionResponseImpl implements WebsocketTypes.ISessionResponse { ServerDispatch.connectionFailed(); } - testConnectionSuccessful(serverOptions: number): void { - ServerDispatch.testConnectionSuccessful(serverOptions); + testConnectionSuccessful(supportsHashedPassword: boolean): void { + ServerDispatch.testConnectionSuccessful(supportsHashedPassword); } testConnectionFailed(): void { diff --git a/webclient/src/components/KnownHosts/useKnownHostsComponent.ts b/webclient/src/components/KnownHosts/useKnownHostsComponent.ts index be6afa056..8ea7a2618 100644 --- a/webclient/src/components/KnownHosts/useKnownHostsComponent.ts +++ b/webclient/src/components/KnownHosts/useKnownHostsComponent.ts @@ -6,7 +6,6 @@ import { LoadingState, useKnownHosts, useReduxEffect, useWebClient } from '@app/ import { getHostPort, HostDTO } from '@app/services'; import { ServerDispatch, ServerSelectors, ServerTypes, useAppSelector } from '@app/store'; import { App } from '@app/types'; -import { passwordSaltSupported } from '@app/websocket'; export enum TestConnection { TESTING = 'testing', @@ -83,14 +82,13 @@ export function useKnownHostsComponent({ testConnection(selectedHost); }, [selectedHost]); - useReduxEffect<{ serverOptions: number }>(({ payload: { serverOptions } }) => { + useReduxEffect<{ supportsHashedPassword: boolean }>(({ payload: { supportsHashedPassword } }) => { const host = pendingTestRef.current; if (!host) { return; } pendingTestRef.current = null; - const supportsHashedPassword = passwordSaltSupported(serverOptions); if (host.id != null && host.supportsHashedPassword !== supportsHashedPassword) { void knownHosts.update(host.id, { supportsHashedPassword }); } diff --git a/webclient/src/store/server/server.actions.spec.ts b/webclient/src/store/server/server.actions.spec.ts index 1b5865369..4c2463eb3 100644 --- a/webclient/src/store/server/server.actions.spec.ts +++ b/webclient/src/store/server/server.actions.spec.ts @@ -42,9 +42,9 @@ describe('Actions', () => { }); it('testConnectionSuccessful', () => { - expect(Actions.testConnectionSuccessful({ serverOptions: 1 })).toEqual({ + expect(Actions.testConnectionSuccessful({ supportsHashedPassword: true })).toEqual({ type: Types.TEST_CONNECTION_SUCCESSFUL, - payload: { serverOptions: 1 }, + payload: { supportsHashedPassword: true }, }); }); diff --git a/webclient/src/store/server/server.dispatch.spec.ts b/webclient/src/store/server/server.dispatch.spec.ts index caa061010..110768a0e 100644 --- a/webclient/src/store/server/server.dispatch.spec.ts +++ b/webclient/src/store/server/server.dispatch.spec.ts @@ -56,9 +56,9 @@ describe('Dispatch', () => { }); it('testConnectionSuccessful dispatches Actions.testConnectionSuccessful()', () => { - Dispatch.testConnectionSuccessful(3); + Dispatch.testConnectionSuccessful(true); expect(mockDispatch).toHaveBeenCalledWith( - Actions.testConnectionSuccessful({ serverOptions: 3 }), + Actions.testConnectionSuccessful({ supportsHashedPassword: true }), ); }); diff --git a/webclient/src/store/server/server.dispatch.ts b/webclient/src/store/server/server.dispatch.ts index e6e74c96b..272599866 100644 --- a/webclient/src/store/server/server.dispatch.ts +++ b/webclient/src/store/server/server.dispatch.ts @@ -25,8 +25,8 @@ export const Dispatch = { testConnectionStarted: () => { store.dispatch(Actions.testConnectionStarted()); }, - testConnectionSuccessful: (serverOptions: number) => { - store.dispatch(Actions.testConnectionSuccessful({ serverOptions })); + testConnectionSuccessful: (supportsHashedPassword: boolean) => { + store.dispatch(Actions.testConnectionSuccessful({ supportsHashedPassword })); }, testConnectionFailed: () => { store.dispatch(Actions.testConnectionFailed()); diff --git a/webclient/src/store/server/server.reducer.ts b/webclient/src/store/server/server.reducer.ts index ef928433e..73774164c 100644 --- a/webclient/src/store/server/server.reducer.ts +++ b/webclient/src/store/server/server.reducer.ts @@ -129,11 +129,11 @@ export const serverSlice = createSlice({ state.testConnectionStatus = 'testing'; }, - // `serverOptions` is typed on the action so `useReduxEffect` subscribers - // (see useKnownHostsComponent) can read it from the dispatched action — - // it's deliberately not stored in state since only the lifecycle matters - // here; the capability bitmask is persisted per-host to Dexie. - testConnectionSuccessful: (state, _action: PayloadAction<{ serverOptions: number }>) => { + // `supportsHashedPassword` is typed on the action so `useReduxEffect` + // subscribers (see useKnownHostsComponent) can persist it to the host + // record in Dexie. It's deliberately not stored in redux state since + // only the lifecycle matters here; per-host capability lives in Dexie. + testConnectionSuccessful: (state, _action: PayloadAction<{ supportsHashedPassword: boolean }>) => { state.testConnectionStatus = 'success'; }, diff --git a/webclient/src/websocket/WebClient.spec.ts b/webclient/src/websocket/WebClient.spec.ts index 98870e7ac..3edc330f0 100644 --- a/webclient/src/websocket/WebClient.spec.ts +++ b/webclient/src/websocket/WebClient.spec.ts @@ -205,23 +205,21 @@ describe('WebClient', () => { expect(MockWS).toHaveBeenCalledWith(expect.stringMatching(/:\/\/server\.example\.com\/servatrice$/)); }); - it('dispatches testConnectionSuccessful with serverOptions on ServerIdentification', () => { + it('dispatches testConnectionSuccessful with supportsHashedPassword=true when the bit is set', () => { client.testConnect(target); const data = buildServerIdentificationMessage({ serverOptions: Event_ServerIdentification_ServerOptions.SupportsPasswordHash, }); wsMockInstance.onmessage({ data: data.buffer }); - expect(mockResponse.session.testConnectionSuccessful).toHaveBeenCalledWith( - Event_ServerIdentification_ServerOptions.SupportsPasswordHash, - ); + expect(mockResponse.session.testConnectionSuccessful).toHaveBeenCalledWith(true); expect(wsMockInstance.close).toHaveBeenCalled(); }); - it('reports success with serverOptions=0 for naked-password servers', () => { + it('dispatches testConnectionSuccessful with supportsHashedPassword=false for naked-password servers', () => { client.testConnect(target); const data = buildServerIdentificationMessage({ serverOptions: 0 }); wsMockInstance.onmessage({ data: data.buffer }); - expect(mockResponse.session.testConnectionSuccessful).toHaveBeenCalledWith(0); + expect(mockResponse.session.testConnectionSuccessful).toHaveBeenCalledWith(false); }); it('fails on protocol-version mismatch instead of reporting success', () => { diff --git a/webclient/src/websocket/WebClient.ts b/webclient/src/websocket/WebClient.ts index f9a217fbe..9e95b587e 100644 --- a/webclient/src/websocket/WebClient.ts +++ b/webclient/src/websocket/WebClient.ts @@ -18,6 +18,7 @@ import { StatusEnum } from './types/StatusEnum'; import { ProtobufService } from './services/ProtobufService'; import { WebSocketService } from './services/WebSocketService'; import { buildWebSocketUrl } from './utils/buildWebSocketUrl'; +import { passwordSaltSupported } from './utils/passwordHasher'; export class WebClient { private static _instance: WebClient | null = null; @@ -96,10 +97,12 @@ export class WebClient { this.testSocket = socket; // "Green" means reachable AND speaking a compatible Cockatrice protocol. - // Waiting for Event_ServerIdentification lets us carry serverOptions back - // to the UI so naked-password hosts can be distinguished without a login. + // Waiting for Event_ServerIdentification lets us read the hashed-password + // capability before the user ever logs in. The bitmask is resolved here + // (the websocket layer owns protocol details) so downstream consumers + // receive a domain-level boolean instead of a raw integer. let resolved = false; - const resolve = (ok: boolean, serverOptions = 0): void => { + const resolve = (ok: boolean, supportsHashedPassword = false): void => { if (resolved) { return; } @@ -109,7 +112,7 @@ export class WebClient { // already taken over and we'd race a stale result into its pending-ref. if (this.testSocket === socket) { if (ok) { - this.response.session.testConnectionSuccessful(serverOptions); + this.response.session.testConnectionSuccessful(supportsHashedPassword); } else { this.response.session.testConnectionFailed(); } @@ -135,7 +138,7 @@ export class WebClient { resolve(false); return; } - resolve(true, ident.serverOptions); + resolve(true, passwordSaltSupported(ident.serverOptions)); } catch { resolve(false); } diff --git a/webclient/src/websocket/types/WebClientResponse.ts b/webclient/src/websocket/types/WebClientResponse.ts index beedf6d79..57638ba8d 100644 --- a/webclient/src/websocket/types/WebClientResponse.ts +++ b/webclient/src/websocket/types/WebClientResponse.ts @@ -55,7 +55,7 @@ export interface ISessionResponse { loginSuccessful(options: LoginSuccessContext): void; loginFailed(): void; connectionFailed(): void; - testConnectionSuccessful(serverOptions: number): void; + testConnectionSuccessful(supportsHashedPassword: boolean): void; testConnectionFailed(): void; updateBuddyList(buddyList: ServerInfo_User[]): void; addToBuddyList(user: ServerInfo_User): void;