Repository navigation
Security quick wins: show deletion auth, debug default off, request size metrics #1455
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+489
−10
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
7b75d03
Security quick wins: admin-gate show deletion, debug off by default, …
Tim020 61e8e36
Add Docker entrypoint upgrade path, measured configurable size limits…
Tim020 3dce9d0
Drop Docker and size-limit changes; add request size histograms
Tim020 19c012c
Address review: persistent 1009 backoff, bounded method label, harden…
Tim020 ce676e3
Address review round 2: resync after 1009 reconnect, cover stability …
Tim020 1748bd9
Address review round 3: make 30s stability test exercise the reset, u…
Tim020 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
147 changes: 147 additions & 0 deletions
147
client-v3/src/composables/useWebSocket.closeTooBig.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; | ||
|
|
||
| // Close code 1009 (message too big) must back off instead of reconnecting at the minimum | ||
| // delay forever. | ||
|
|
||
| vi.mock('@/router', () => ({ default: { currentRoute: { value: { path: '/' } }, push: vi.fn() } })); | ||
| vi.mock('@/js/toast', () => ({ | ||
| toast: { info: vi.fn(), error: vi.fn(), success: vi.fn() }, | ||
| })); | ||
|
|
||
| class FakeWebSocket { | ||
| static OPEN = 1; | ||
| static CONNECTING = 0; | ||
| static instances: FakeWebSocket[] = []; | ||
| readyState = 3; | ||
| onopen: (() => Promise<void> | void) | null = null; | ||
| onmessage: ((e: { data: string }) => void) | null = null; | ||
| onclose: ((e?: { code: number }) => void) | null = null; | ||
| onerror: (() => void) | null = null; | ||
|
|
||
| constructor() { | ||
| FakeWebSocket.instances.push(this); | ||
| } | ||
|
|
||
| send(): void {} | ||
| } | ||
|
|
||
| // The composable keeps its backoff state at module level, so load a fresh copy per test. | ||
| let useWebSocket: typeof import('./useWebSocket').useWebSocket; | ||
| let toast: typeof import('@/js/toast').toast; | ||
|
|
||
| describe('close code 1009', () => { | ||
| beforeEach(async () => { | ||
| vi.resetModules(); | ||
| FakeWebSocket.instances = []; | ||
| vi.useFakeTimers(); | ||
| vi.stubGlobal('WebSocket', FakeWebSocket); | ||
| const { createPinia, setActivePinia } = await import('pinia'); | ||
| setActivePinia(createPinia()); | ||
| ({ useWebSocket } = await import('./useWebSocket')); | ||
| ({ toast } = await import('@/js/toast')); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.useRealTimers(); | ||
| vi.unstubAllGlobals(); | ||
| }); | ||
|
|
||
| it('re-syncs the live session after reconnecting from a 1009 close', async () => { | ||
| const { useShowStore } = await import('@/stores/show'); | ||
| const showStore = useShowStore(); | ||
| showStore.currentSession = { id: 1 } as never; | ||
| const resync = vi.spyOn(showStore, 'getShowSessionData').mockResolvedValue(); | ||
|
|
||
| useWebSocket().connect(); | ||
| FakeWebSocket.instances[0].onclose?.({ code: 1009 }); | ||
| vi.advanceTimersByTime(2100); | ||
| await FakeWebSocket.instances[1].onopen?.(); | ||
|
|
||
| expect(resync).toHaveBeenCalledTimes(1); | ||
| expect(toast.success).toHaveBeenCalledWith('WebSocket reconnected after 1 attempt'); | ||
| }); | ||
|
|
||
| it('forgets earlier 1009 closes once a connection has stayed up for 30s', async () => { | ||
| useWebSocket().connect(); | ||
| FakeWebSocket.instances[0].onclose?.({ code: 1009 }); | ||
| vi.advanceTimersByTime(2100); | ||
| // Second connection stays up long enough to clear the earlier 1009 count. | ||
| await FakeWebSocket.instances[1].onopen?.(); | ||
| vi.advanceTimersByTime(30100); | ||
| FakeWebSocket.instances[1].onclose?.({ code: 1009 }); | ||
| // Count was cleared, so this is a first close again: 2s delay, not 4s. | ||
| vi.advanceTimersByTime(2100); | ||
| expect(FakeWebSocket.instances).toHaveLength(3); | ||
| }); | ||
|
|
||
| it('keeps the 1009 count when the connection was up for less than 30s', async () => { | ||
| useWebSocket().connect(); | ||
| FakeWebSocket.instances[0].onclose?.({ code: 1009 }); | ||
| vi.advanceTimersByTime(2100); | ||
| await FakeWebSocket.instances[1].onopen?.(); | ||
| vi.advanceTimersByTime(10000); | ||
| FakeWebSocket.instances[1].onclose?.({ code: 1009 }); | ||
| // Second consecutive close: 4s delay. | ||
| vi.advanceTimersByTime(3500); | ||
| expect(FakeWebSocket.instances).toHaveLength(2); | ||
| vi.advanceTimersByTime(600); | ||
| expect(FakeWebSocket.instances).toHaveLength(3); | ||
| }); | ||
|
|
||
| it('keeps backing off when authentication succeeds between closes', async () => { | ||
| useWebSocket().connect(); | ||
| FakeWebSocket.instances[0].onclose?.({ code: 1009 }); | ||
| vi.advanceTimersByTime(2100); | ||
| expect(FakeWebSocket.instances).toHaveLength(2); | ||
|
|
||
| // Reconnect and re-authenticate (this resets the generic error counter). | ||
| FakeWebSocket.instances[1].onmessage?.({ data: JSON.stringify({ OP: 'WS_AUTH_SUCCESS' }) }); | ||
| await vi.advanceTimersByTimeAsync(0); | ||
| FakeWebSocket.instances[1].onclose?.({ code: 1009 }); | ||
|
|
||
| // Delay must have doubled to 4s rather than starting over at 2s. | ||
| vi.advanceTimersByTime(3500); | ||
| expect(FakeWebSocket.instances).toHaveLength(2); | ||
| vi.advanceTimersByTime(600); | ||
| expect(FakeWebSocket.instances).toHaveLength(3); | ||
| expect(toast.error).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('reconnects even if showing the toast throws', () => { | ||
| vi.mocked(toast.error).mockImplementationOnce(() => { | ||
| throw new Error('toast failed'); | ||
| }); | ||
| useWebSocket().connect(); | ||
| FakeWebSocket.instances[0].onclose?.({ code: 1009 }); | ||
| vi.advanceTimersByTime(2100); | ||
| expect(FakeWebSocket.instances).toHaveLength(2); | ||
| }); | ||
|
|
||
| it('does not back off or notify on a normal close', () => { | ||
| useWebSocket().connect(); | ||
| FakeWebSocket.instances[0].onclose?.({ code: 1000 }); | ||
| expect(toast.error).not.toHaveBeenCalled(); | ||
| vi.advanceTimersByTime(1100); | ||
| expect(FakeWebSocket.instances).toHaveLength(2); | ||
| }); | ||
|
|
||
| it('notifies the user once and backs off between reconnects', () => { | ||
|
Tim020 marked this conversation as resolved.
|
||
| useWebSocket().connect(); | ||
| FakeWebSocket.instances[0].onclose?.({ code: 1009 }); | ||
| expect(toast.error).toHaveBeenCalledTimes(1); | ||
|
|
||
| // First reconnect delay is 2s (1s * 2^1): nothing after 1.5s, a new socket after 2s. | ||
| vi.advanceTimersByTime(1500); | ||
| expect(FakeWebSocket.instances).toHaveLength(1); | ||
| vi.advanceTimersByTime(600); | ||
| expect(FakeWebSocket.instances).toHaveLength(2); | ||
|
|
||
| FakeWebSocket.instances[1].onclose?.({ code: 1009 }); | ||
| expect(toast.error).toHaveBeenCalledTimes(1); | ||
| // Second delay doubles to 4s. | ||
| vi.advanceTimersByTime(3500); | ||
| expect(FakeWebSocket.instances).toHaveLength(2); | ||
| vi.advanceTimersByTime(600); | ||
| expect(FakeWebSocket.instances).toHaveLength(3); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.