mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 08:03:28 +00:00
fix: address remote pairing review findings
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { encodePairingOffer, PAIRING_OFFER_VERSION } from '../../../../shared/pairing'
|
||||
import { parseHostAccessLink } from '../../../../shared/remote-pairing-address'
|
||||
import { RemoteServerFields } from './AddRemoteHostFields'
|
||||
|
||||
function loopbackAccessLink(): string {
|
||||
return encodePairingOffer({
|
||||
v: PAIRING_OFFER_VERSION,
|
||||
endpoint: 'ws://127.0.0.1:6768',
|
||||
deviceToken: 'token',
|
||||
publicKeyB64: 'key',
|
||||
scope: 'runtime'
|
||||
})
|
||||
}
|
||||
|
||||
describe('RemoteServerFields', () => {
|
||||
it('associates blocked loopback guidance with the access-link input', () => {
|
||||
const pairingCode = loopbackAccessLink()
|
||||
const markup = renderToStaticMarkup(
|
||||
<RemoteServerFields
|
||||
name="Remote workstation"
|
||||
pairingCode={pairingCode}
|
||||
parsedLink={parseHostAccessLink(pairingCode)}
|
||||
disabled={false}
|
||||
onNameChange={vi.fn()}
|
||||
onPairingCodeChange={vi.fn()}
|
||||
allowLoopback={false}
|
||||
onAllowLoopbackChange={vi.fn()}
|
||||
onSubmit={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(markup).toContain('aria-invalid="true"')
|
||||
expect(markup).toContain('aria-describedby="add-server-loopback-blocked"')
|
||||
expect(markup).toContain('id="add-server-loopback-blocked"')
|
||||
})
|
||||
})
|
||||
@@ -148,6 +148,11 @@ export function RemoteServerFields({
|
||||
const inputError = pairingCode.trim() !== '' && !parsedLink.ok
|
||||
const loopbackBlocked =
|
||||
parsedLink.ok && parsedLink.value.endpointKind === 'loopback' && !allowLoopback
|
||||
const pairingCodeDescriptionId = inputError
|
||||
? 'add-server-pairing-code-error'
|
||||
: loopbackBlocked
|
||||
? 'add-server-loopback-blocked'
|
||||
: 'add-server-pairing-code-help'
|
||||
return (
|
||||
<form
|
||||
className="space-y-3"
|
||||
@@ -178,8 +183,8 @@ export function RemoteServerFields({
|
||||
</Label>
|
||||
<Input
|
||||
id="add-server-pairing-code"
|
||||
aria-invalid={inputError}
|
||||
aria-describedby="add-server-pairing-code-help"
|
||||
aria-invalid={inputError || loopbackBlocked}
|
||||
aria-describedby={pairingCodeDescriptionId}
|
||||
value={pairingCode}
|
||||
disabled={disabled}
|
||||
onChange={(event) => onPairingCodeChange(event.target.value)}
|
||||
@@ -196,7 +201,7 @@ export function RemoteServerFields({
|
||||
)}
|
||||
</p>
|
||||
{inputError ? (
|
||||
<p role="alert" className="text-xs text-destructive">
|
||||
<p id="add-server-pairing-code-error" role="alert" className="text-xs text-destructive">
|
||||
{parsedLink.ok ? null : translateHostAccessLinkError(parsedLink.kind)}
|
||||
</p>
|
||||
) : null}
|
||||
@@ -239,7 +244,7 @@ export function RemoteServerFields({
|
||||
</div>
|
||||
) : null}
|
||||
{loopbackBlocked ? (
|
||||
<p role="alert" className="text-xs text-destructive">
|
||||
<p id="add-server-loopback-blocked" role="alert" className="text-xs text-destructive">
|
||||
{translate(
|
||||
'auto.components.sidebar.AddRemoteHostDialog.loopbackBlocked',
|
||||
'Enable the SSH tunnel override or create a new link using the other host’s Tailscale or LAN address.'
|
||||
|
||||
@@ -631,6 +631,35 @@ describe('web runtime environment identity', () => {
|
||||
{ id: 'web-server-a' }
|
||||
])
|
||||
})
|
||||
|
||||
it('classifies coded browser authorization failures without relying on copy', async () => {
|
||||
vi.doMock('./web-runtime-client', () => ({
|
||||
WebRuntimeClient: class {
|
||||
call(): Promise<RuntimeRpcResponse<unknown>> {
|
||||
return Promise.reject(
|
||||
Object.assign(new Error('Access grant rejected.'), { code: 'unauthorized' })
|
||||
)
|
||||
}
|
||||
|
||||
close(): void {}
|
||||
}
|
||||
}))
|
||||
const globals = installBrowserGlobals('Linux')
|
||||
writeStoredRuntimeEnvironment(globals.storage, 'web-server-a')
|
||||
const { installWebPreloadApi } = await import('./web-preload-api')
|
||||
installWebPreloadApi()
|
||||
|
||||
await expect(
|
||||
globals.window.api.runtimeEnvironments.verifyAndAddFromPairingCode({
|
||||
name: 'Expired server',
|
||||
pairingCode: encodePairingCode()
|
||||
})
|
||||
).resolves.toMatchObject({
|
||||
ok: false,
|
||||
kind: 'access-link-invalid',
|
||||
message: 'Access grant rejected.'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('web browser-local port capability', () => {
|
||||
|
||||
@@ -115,6 +115,7 @@ import {
|
||||
import { parseWebPairingInput } from './web-pairing'
|
||||
import { copyClipboardTextViaExecCommand } from './web-clipboard-copy-fallback'
|
||||
import { WebRuntimeClient } from './web-runtime-client'
|
||||
import { isWebRuntimeUnauthorizedError } from './web-runtime-client-error'
|
||||
import { RuntimeRpcCallQueuePool } from '../../../shared/runtime-rpc-call-queue'
|
||||
import {
|
||||
assertClipboardTextWriteWithinLimitWithYield,
|
||||
@@ -1408,7 +1409,10 @@ function createRuntimeEnvironmentsApi(): NonNullable<Partial<PreloadApi>['runtim
|
||||
)
|
||||
}
|
||||
}
|
||||
if (error instanceof Error && error.message.startsWith('Unauthorized.')) {
|
||||
if (
|
||||
isWebRuntimeUnauthorizedError(error) ||
|
||||
(error instanceof Error && error.message.startsWith('Unauthorized.'))
|
||||
) {
|
||||
return {
|
||||
ok: false,
|
||||
kind: 'access-link-invalid',
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
const UNAUTHORIZED_MESSAGE = 'Unauthorized. Pair this web client again.'
|
||||
|
||||
export class WebRuntimeClientError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly code: 'unauthorized'
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'WebRuntimeClientError'
|
||||
}
|
||||
}
|
||||
|
||||
export function createWebRuntimeUnauthorizedError(): WebRuntimeClientError {
|
||||
return new WebRuntimeClientError(UNAUTHORIZED_MESSAGE, 'unauthorized')
|
||||
}
|
||||
|
||||
export function isWebRuntimeUnauthorizedError(
|
||||
error: unknown
|
||||
): error is Error & { code: 'unauthorized' } {
|
||||
return (
|
||||
error instanceof Error &&
|
||||
'code' in error &&
|
||||
(error as { code?: unknown }).code === 'unauthorized'
|
||||
)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
publicKeyToBase64
|
||||
} from './web-e2ee'
|
||||
import { SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY } from '../../../shared/protocol-version'
|
||||
import { createWebRuntimeUnauthorizedError } from './web-runtime-client-error'
|
||||
|
||||
type WebRuntimeConnectionState =
|
||||
| 'disconnected'
|
||||
@@ -478,7 +479,7 @@ export class WebRuntimeClient {
|
||||
} else if (control.type === 'e2ee_error' || control.error?.code === 'unauthorized') {
|
||||
this.intentionallyClosed = true
|
||||
this.setState('auth-failed')
|
||||
this.rejectAllPending('Unauthorized. Pair this web client again.')
|
||||
this.rejectAllPending(createWebRuntimeUnauthorizedError())
|
||||
this.notifySubscriptionsError('unauthorized', 'Unauthorized. Pair this web client again.')
|
||||
this.ws?.close()
|
||||
}
|
||||
@@ -530,7 +531,7 @@ export class WebRuntimeClient {
|
||||
if (isRuntimeFailureResponse(response) && response.error.code === 'unauthorized') {
|
||||
this.intentionallyClosed = true
|
||||
this.setState('auth-failed')
|
||||
this.rejectAllPending('Unauthorized. Pair this web client again.')
|
||||
this.rejectAllPending(createWebRuntimeUnauthorizedError())
|
||||
this.notifySubscriptionsError('unauthorized', 'Unauthorized. Pair this web client again.')
|
||||
this.ws?.close()
|
||||
return
|
||||
@@ -584,7 +585,7 @@ export class WebRuntimeClient {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (this.state === 'auth-failed') {
|
||||
return Promise.reject(new Error('Unauthorized. Pair this web client again.'))
|
||||
return Promise.reject(createWebRuntimeUnauthorizedError())
|
||||
}
|
||||
if (this.intentionallyClosed) {
|
||||
return Promise.reject(new Error('Remote Orca runtime connection closed.'))
|
||||
@@ -658,7 +659,7 @@ export class WebRuntimeClient {
|
||||
waiter.resolve()
|
||||
}
|
||||
} else if (next === 'auth-failed') {
|
||||
this.rejectAllWaiters(new Error('Unauthorized. Pair this web client again.'))
|
||||
this.rejectAllWaiters(createWebRuntimeUnauthorizedError())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -667,8 +668,8 @@ export class WebRuntimeClient {
|
||||
return `web-rpc-${this.requestCounter}-${Date.now()}`
|
||||
}
|
||||
|
||||
private rejectAllPending(reason: string): void {
|
||||
const error = new Error(reason)
|
||||
private rejectAllPending(reason: string | Error): void {
|
||||
const error = typeof reason === 'string' ? new Error(reason) : reason
|
||||
for (const [id, pending] of this.pending) {
|
||||
this.pending.delete(id)
|
||||
window.clearTimeout(pending.timeout)
|
||||
|
||||
@@ -69,6 +69,10 @@ describe('remote pairing address', () => {
|
||||
ok: false,
|
||||
kind: 'non-connectable-destination'
|
||||
})
|
||||
expect(parseHostAccessLink(accessLink('wss://orca.example.com:0'))).toMatchObject({
|
||||
ok: false,
|
||||
kind: 'non-connectable-destination'
|
||||
})
|
||||
})
|
||||
|
||||
it('blocks absolute localhost names used in access links', () => {
|
||||
|
||||
@@ -127,7 +127,8 @@ export function parseHostAccessLink(input: string): ParseHostAccessLinkResult {
|
||||
if (
|
||||
normalizedHostname === '0.0.0.0' ||
|
||||
normalizedHostname === '::' ||
|
||||
getEmbeddedIPv4Address(normalizedHostname) === '0.0.0.0'
|
||||
getEmbeddedIPv4Address(normalizedHostname) === '0.0.0.0' ||
|
||||
endpoint.port === '0'
|
||||
) {
|
||||
return {
|
||||
ok: false,
|
||||
|
||||
Reference in New Issue
Block a user