mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 00:02:39 +00:00
* fix(browser): apply the app-wide HTTP proxy to embedded browser sessions The proxy setting was only ever written to `session.defaultSession`, but browser guests run on their own `persist:orca-*` partitions. Any host reachable only via the configured proxy failed to load in an embedded tab, landing on `chrome-error://chromewebdata/`, while the same setting worked everywhere else. Adds a per-session applier alongside the existing defaultSession path, keyed by a WeakMap so one session's applied config can't suppress another's, and applies it to every browser partition through the single installer they all pass through. Startup awaits an explicit sweep so the first guest navigation can't race the installer's fire-and-forget write, and a settings change re-sweeps so toggling the proxy takes effect without a restart. Env-var fallback and the system-proxy probe mirror the defaultSession behaviour, so a browser partition resolves the proxy the same way the rest of the app does. Fixes STA-4779 * fix(browser): await per-session proxy readiness * fix(proxy): preserve loopback and authenticate * fix(proxy): settle browser partition update races * fix(proxy): close partition policy races * fix(proxy): order settings and release removed sessions * test(browser): await partition proxy readiness * fix(proxy): cancel removed partition retries * refactor(proxy): keep OpenCode rate limits out of scope * fix(proxy): preserve sessionless host policy * fix(proxy): gate requests on policy readiness * fix(proxy): retire deleted browser sessions * fix(proxy): close retired browser guests * fix(proxy): retain retired session guards * fix(proxy): retain retired partition policies * fix(browser): retry transient proxy application failures * fix(browser): release deleted partition installer state * fix(proxy): retry delayed transient failures * fix(proxy): preserve route session authority after rebase * fix(proxy): clear retired session credentials * fix(proxy): retire failed browser profiles * fix(proxy): harden failed session cleanup --------- Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
239 lines
8.1 KiB
TypeScript
239 lines
8.1 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const { sessionsByPartition, fromPartition, fromPartitionMock } = vi.hoisted(() => {
|
|
const sessionsByPartition = new Map<string, Record<string, ReturnType<typeof vi.fn>>>()
|
|
const fromPartition = (partition: string): Record<string, ReturnType<typeof vi.fn>> => {
|
|
const existing = sessionsByPartition.get(partition)
|
|
if (existing) {
|
|
return existing
|
|
}
|
|
const created = {
|
|
resolveProxy: vi.fn(async () => 'DIRECT'),
|
|
setProxy: vi.fn(async () => {}),
|
|
closeAllConnections: vi.fn(async () => {})
|
|
}
|
|
sessionsByPartition.set(partition, created)
|
|
return created
|
|
}
|
|
return { sessionsByPartition, fromPartition, fromPartitionMock: vi.fn(fromPartition) }
|
|
})
|
|
|
|
vi.mock('electron', () => ({
|
|
session: {
|
|
defaultSession: { resolveProxy: vi.fn(async () => 'DIRECT'), setProxy: vi.fn(async () => {}) },
|
|
fromPartition: fromPartitionMock
|
|
}
|
|
}))
|
|
|
|
import {
|
|
applyBrowserSessionProxies,
|
|
applyProxyToBrowserSession,
|
|
invalidateBrowserSessionProxyApplication,
|
|
setBrowserNetworkProxySettingsResolver
|
|
} from './browser-session-proxy'
|
|
import { handleElectronProxyLogin } from '../network/electron-proxy-credentials'
|
|
import { releaseProxySessionApplication } from '../network/proxy-settings'
|
|
|
|
const PROFILES = [
|
|
{
|
|
id: 'default',
|
|
scope: 'default' as const,
|
|
partition: 'persist:orca-browser',
|
|
label: 'Default',
|
|
source: null
|
|
},
|
|
{
|
|
id: 'iso',
|
|
scope: 'isolated' as const,
|
|
partition: 'persist:orca-browser-session-iso',
|
|
label: 'Isolated',
|
|
source: null
|
|
}
|
|
]
|
|
|
|
describe('browser session proxy', () => {
|
|
beforeEach(() => {
|
|
sessionsByPartition.clear()
|
|
fromPartitionMock.mockReset().mockImplementation(fromPartition)
|
|
setBrowserNetworkProxySettingsResolver(null)
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
// Browser partitions cannot inherit a proxy pinned to defaultSession.
|
|
it('pins the configured proxy onto every browser partition', async () => {
|
|
await applyBrowserSessionProxies(PROFILES, {
|
|
httpProxyUrl: 'socks5://127.0.0.1:1080',
|
|
httpProxyBypassRules: ''
|
|
})
|
|
|
|
expect(fromPartitionMock).toHaveBeenCalledWith('persist:orca-browser')
|
|
expect(fromPartitionMock).toHaveBeenCalledWith('persist:orca-browser-session-iso')
|
|
for (const partition of PROFILES.map((p) => p.partition)) {
|
|
expect(sessionsByPartition.get(partition)?.setProxy).toHaveBeenCalledWith({
|
|
mode: 'fixed_servers',
|
|
proxyRules: 'socks5://127.0.0.1:1080'
|
|
})
|
|
}
|
|
})
|
|
|
|
it('carries bypass rules through to each partition', async () => {
|
|
await applyBrowserSessionProxies(PROFILES, {
|
|
httpProxyUrl: 'socks5://127.0.0.1:1080',
|
|
httpProxyBypassRules: 'localhost, *.internal'
|
|
})
|
|
|
|
expect(sessionsByPartition.get('persist:orca-browser')?.setProxy).toHaveBeenCalledWith({
|
|
mode: 'fixed_servers',
|
|
proxyRules: 'socks5://127.0.0.1:1080',
|
|
proxyBypassRules: 'localhost;*.internal'
|
|
})
|
|
})
|
|
|
|
it('authenticates a credentialed proxy without exposing credentials in partition rules', async () => {
|
|
await applyBrowserSessionProxies(PROFILES.slice(0, 1), {
|
|
httpProxyUrl: 'http://browser-user:browser-pass@proxy.example:8080',
|
|
httpProxyBypassRules: ''
|
|
})
|
|
const sess = sessionsByPartition.get('persist:orca-browser')
|
|
expect(sess?.setProxy).toHaveBeenCalledWith({
|
|
mode: 'fixed_servers',
|
|
proxyRules: 'http://proxy.example:8080'
|
|
})
|
|
|
|
const preventDefault = vi.fn()
|
|
const callback = vi.fn()
|
|
handleElectronProxyLogin(
|
|
{ preventDefault } as never,
|
|
{ session: sess } as never,
|
|
{} as never,
|
|
{
|
|
isProxy: true,
|
|
scheme: 'basic',
|
|
host: 'proxy.example',
|
|
port: 8080,
|
|
realm: 'proxy'
|
|
},
|
|
callback
|
|
)
|
|
|
|
expect(preventDefault).toHaveBeenCalledOnce()
|
|
expect(callback).toHaveBeenCalledWith('browser-user', 'browser-pass')
|
|
})
|
|
|
|
it('keeps sweeping but reports failure so the failed partition stays gated', async () => {
|
|
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
fromPartitionMock.mockImplementationOnce(() => {
|
|
throw new Error('partition unavailable')
|
|
})
|
|
|
|
await expect(
|
|
applyBrowserSessionProxies(PROFILES, {
|
|
httpProxyUrl: 'socks5://127.0.0.1:1080',
|
|
httpProxyBypassRules: ''
|
|
})
|
|
).rejects.toThrow('persist:orca-browser')
|
|
|
|
expect(
|
|
sessionsByPartition.get('persist:orca-browser-session-iso')?.setProxy
|
|
).toHaveBeenCalledWith({ mode: 'fixed_servers', proxyRules: 'socks5://127.0.0.1:1080' })
|
|
expect(warn).toHaveBeenCalledWith(
|
|
'[proxy] Failed to apply proxy to browser partition',
|
|
'persist:orca-browser'
|
|
)
|
|
})
|
|
|
|
it('starts every partition write without waiting for earlier partitions', async () => {
|
|
let finishFirstWrite: (() => void) | undefined
|
|
const firstSession = fromPartition('persist:orca-browser')
|
|
firstSession.setProxy.mockImplementation(
|
|
() => new Promise<void>((resolve) => (finishFirstWrite = resolve))
|
|
)
|
|
sessionsByPartition.set('persist:orca-browser', firstSession)
|
|
|
|
const sweep = applyBrowserSessionProxies(PROFILES, {
|
|
httpProxyUrl: 'socks5://127.0.0.1:1080',
|
|
httpProxyBypassRules: ''
|
|
})
|
|
|
|
await vi.waitFor(() =>
|
|
expect(
|
|
sessionsByPartition.get('persist:orca-browser-session-iso')?.setProxy
|
|
).toHaveBeenCalledOnce()
|
|
)
|
|
finishFirstWrite?.()
|
|
await sweep
|
|
})
|
|
|
|
it('settles an in-flight new partition on the latest swept policy', async () => {
|
|
let proxyUrl = 'http://old.example:8080'
|
|
let finishFirstWrite: (() => void) | undefined
|
|
setBrowserNetworkProxySettingsResolver(() => ({ httpProxyUrl: proxyUrl }))
|
|
const sess = fromPartition('persist:orca-browser-session-pending')
|
|
sess.setProxy.mockImplementationOnce(
|
|
() => new Promise<void>((resolve) => (finishFirstWrite = resolve))
|
|
)
|
|
|
|
const pendingInstallation = applyProxyToBrowserSession(sess as never)
|
|
await vi.waitFor(() => expect(sess.setProxy).toHaveBeenCalledTimes(1))
|
|
proxyUrl = 'http://new.example:8080'
|
|
await applyBrowserSessionProxies([], { httpProxyUrl: proxyUrl })
|
|
finishFirstWrite?.()
|
|
await pendingInstallation
|
|
|
|
expect(sess.setProxy.mock.calls).toEqual([
|
|
[{ mode: 'fixed_servers', proxyRules: 'http://old.example:8080' }],
|
|
[{ mode: 'fixed_servers', proxyRules: 'http://new.example:8080' }]
|
|
])
|
|
})
|
|
|
|
it('does not reapply policy after an in-flight partition is invalidated', async () => {
|
|
let proxyUrl = 'http://old.example:8080'
|
|
let finishFirstWrite: (() => void) | undefined
|
|
setBrowserNetworkProxySettingsResolver(() => ({ httpProxyUrl: proxyUrl }))
|
|
const sess = fromPartition('persist:orca-browser-session-restored')
|
|
sess.setProxy.mockImplementationOnce(
|
|
() => new Promise<void>((resolve) => (finishFirstWrite = resolve))
|
|
)
|
|
|
|
const restoredInstallation = applyProxyToBrowserSession(sess as never)
|
|
await vi.waitFor(() => expect(sess.setProxy).toHaveBeenCalledTimes(1))
|
|
invalidateBrowserSessionProxyApplication(sess as never)
|
|
const release = releaseProxySessionApplication(sess as never)
|
|
proxyUrl = 'http://new.example:8080'
|
|
await applyBrowserSessionProxies([], { httpProxyUrl: proxyUrl })
|
|
finishFirstWrite?.()
|
|
await Promise.all([restoredInstallation, release])
|
|
|
|
expect(sess.setProxy.mock.calls).toEqual([
|
|
[{ mode: 'fixed_servers', proxyRules: 'http://old.example:8080' }],
|
|
[{ mode: 'system' }]
|
|
])
|
|
})
|
|
|
|
it('reads settings through the injected resolver when none are passed', async () => {
|
|
setBrowserNetworkProxySettingsResolver(() => ({
|
|
httpProxyUrl: 'socks5://127.0.0.1:1080',
|
|
httpProxyBypassRules: ''
|
|
}))
|
|
const sess = fromPartitionMock('persist:orca-browser')
|
|
|
|
await applyProxyToBrowserSession(sess as never)
|
|
|
|
expect(sess.setProxy).toHaveBeenCalledWith({
|
|
mode: 'fixed_servers',
|
|
proxyRules: 'socks5://127.0.0.1:1080'
|
|
})
|
|
})
|
|
|
|
it('makes no proxy write when no resolver is registered', async () => {
|
|
const sess = fromPartitionMock('persist:orca-browser')
|
|
|
|
await applyProxyToBrowserSession(sess as never)
|
|
|
|
expect(sess.setProxy).not.toHaveBeenCalled()
|
|
})
|
|
})
|