The IPPLifecycleHelper handles if a devices goes to sleep, because during that time the proxy-pass might have expired. It makes sure we don't send requests with exipired tokens which would cause broken connections. For android we need similar behavior however hooked into foregrounding/backgrounding of the application. Differential Revision: https://phabricator.services.mozilla.com/D308313
268 lines
7.5 KiB
JavaScript
268 lines
7.5 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
const { IPPLifecycleHelper } = ChromeUtils.importESModule(
|
|
"moz-src:///toolkit/components/ipprotection/IPPLifecycleHelper.sys.mjs"
|
|
);
|
|
|
|
const SLEEP_TOPIC = "sleep_notification";
|
|
const WAKE_TOPIC = "wake_notification";
|
|
|
|
add_setup(async function () {
|
|
await putServerInRemoteSettings();
|
|
});
|
|
|
|
/**
|
|
* Brings the proxy to the ACTIVE state, at which point the lifecycle helper is
|
|
* observing sleep/wake.
|
|
*/
|
|
async function startActiveProxy() {
|
|
const readyEvent = waitForEvent(
|
|
IPProtectionService,
|
|
"IPProtectionService:StateChanged",
|
|
() => IPProtectionService.state === IPProtectionStates.READY
|
|
);
|
|
IPProtectionService.init();
|
|
await readyEvent;
|
|
|
|
const activeEvent = waitForEvent(
|
|
IPPProxyManager,
|
|
"IPPProxyManager:StateChanged",
|
|
() => IPPProxyManager.state === IPPProxyStates.ACTIVE
|
|
);
|
|
await IPPProxyManager.start();
|
|
await activeEvent;
|
|
}
|
|
|
|
/**
|
|
* On sleep, the channel filter is suspended (proxyInfo cleared).
|
|
*/
|
|
add_task(async function test_sleep_suspends_connection() {
|
|
setupStubs({ validProxyPass: true });
|
|
await startActiveProxy();
|
|
|
|
const channelFilter = IPPProxyManager.channelFilter();
|
|
Assert.ok(channelFilter.proxyInfo, "Connection has proxyInfo while active");
|
|
Assert.ok(IPPProxyManager.isolationKey, "Connection has an isolation key");
|
|
|
|
Services.obs.notifyObservers(null, SLEEP_TOPIC);
|
|
|
|
Assert.equal(
|
|
channelFilter.proxyInfo,
|
|
null,
|
|
"Channel filter is suspended after sleep"
|
|
);
|
|
|
|
await IPPProxyManager.stop();
|
|
IPProtectionService.uninit();
|
|
});
|
|
|
|
/**
|
|
* On wake with a still-good pass, the saved isolation key is re-injected
|
|
* (same key, no rotation).
|
|
*/
|
|
add_task(async function test_wake_with_valid_pass_resumes_same_key() {
|
|
let sandbox = sinon.createSandbox();
|
|
setupStubs({ validProxyPass: true });
|
|
await startActiveProxy();
|
|
|
|
const channelFilter = IPPProxyManager.channelFilter();
|
|
const savedKey = IPPProxyManager.isolationKey;
|
|
|
|
const rotateSpy = sandbox.spy(IPPProxyManager, "rotateProxyPass");
|
|
|
|
Services.obs.notifyObservers(null, SLEEP_TOPIC);
|
|
Assert.equal(channelFilter.proxyInfo, null, "Suspended after sleep");
|
|
|
|
Services.obs.notifyObservers(null, WAKE_TOPIC);
|
|
|
|
Assert.ok(channelFilter.proxyInfo, "Connection resumed after wake");
|
|
Assert.equal(
|
|
IPPProxyManager.isolationKey,
|
|
savedKey,
|
|
"Resuming reuses the isolation key captured before sleep"
|
|
);
|
|
Assert.ok(rotateSpy.notCalled, "A valid pass should not trigger a rotation");
|
|
|
|
await IPPProxyManager.stop();
|
|
IPProtectionService.uninit();
|
|
sandbox.restore();
|
|
});
|
|
|
|
/**
|
|
* On wake with an expired pass, the helper triggers a rotation, which yields a
|
|
* fresh isolation key.
|
|
*/
|
|
add_task(async function test_wake_with_expired_pass_rotates() {
|
|
let sandbox = sinon.createSandbox();
|
|
setupStubs({ validProxyPass: true });
|
|
await startActiveProxy();
|
|
|
|
// Rotate to an expired pass while staying active so canResume is false on the
|
|
// next wake.
|
|
setupStubs({ validProxyPass: false });
|
|
await IPPProxyManager.rotateProxyPass();
|
|
Assert.ok(
|
|
!IPPProxyManager.hasValidProxyPass,
|
|
"Pass is expired after rotating to an expired pass"
|
|
);
|
|
|
|
const savedKey = IPPProxyManager.isolationKey;
|
|
const rotateSpy = sandbox.spy(IPPProxyManager, "rotateProxyPass");
|
|
|
|
Services.obs.notifyObservers(null, SLEEP_TOPIC);
|
|
|
|
// The wake rotation should fetch a fresh, valid pass.
|
|
setupStubs({ validProxyPass: true });
|
|
Services.obs.notifyObservers(null, WAKE_TOPIC);
|
|
|
|
Assert.ok(rotateSpy.called, "An expired pass should trigger a rotation");
|
|
await rotateSpy.returnValues[0];
|
|
|
|
Assert.ok(channelFilterProxyInfoPresent(), "Connection has proxyInfo again");
|
|
Assert.notEqual(
|
|
IPPProxyManager.isolationKey,
|
|
savedKey,
|
|
"Rotation produces a new isolation key"
|
|
);
|
|
Assert.equal(
|
|
IPPProxyManager.state,
|
|
IPPProxyStates.ACTIVE,
|
|
"Proxy stays active across a wake rotation"
|
|
);
|
|
|
|
await IPPProxyManager.stop();
|
|
IPProtectionService.uninit();
|
|
sandbox.restore();
|
|
});
|
|
|
|
/**
|
|
* On wake with a pass that is valid but already inside its rotation window, the
|
|
* helper rotates instead of resuming (validates the canResume threshold).
|
|
*/
|
|
add_task(async function test_wake_within_rotation_window_rotates() {
|
|
let sandbox = sinon.createSandbox();
|
|
setupStubs({ validProxyPass: true });
|
|
await startActiveProxy();
|
|
|
|
// Rotate to a pass that is still valid but due for rotation (expires in 30s,
|
|
// well within the 2 minute rotation window).
|
|
const now = Temporal.Now.instant();
|
|
const soonToExpire = new ProxyPass(
|
|
createProxyPassToken(now, now.add({ seconds: 30 }))
|
|
);
|
|
IPPDummyAuthProvider.setProxyPass({
|
|
status: 200,
|
|
error: undefined,
|
|
pass: soonToExpire,
|
|
usage: new ProxyUsage(
|
|
"5368709120",
|
|
"4294967296",
|
|
"3026-02-01T00:00:00.000Z"
|
|
),
|
|
});
|
|
await IPPProxyManager.rotateProxyPass();
|
|
|
|
Assert.ok(
|
|
IPPProxyManager.hasValidProxyPass,
|
|
"Pass is still valid before its expiry"
|
|
);
|
|
Assert.ok(
|
|
!IPPProxyManager.channelFilter().canResume,
|
|
"Pass within the rotation window cannot be resumed"
|
|
);
|
|
|
|
const savedKey = IPPProxyManager.isolationKey;
|
|
const rotateSpy = sandbox.spy(IPPProxyManager, "rotateProxyPass");
|
|
|
|
Services.obs.notifyObservers(null, SLEEP_TOPIC);
|
|
|
|
setupStubs({ validProxyPass: true });
|
|
Services.obs.notifyObservers(null, WAKE_TOPIC);
|
|
|
|
Assert.ok(
|
|
rotateSpy.called,
|
|
"A pass within the rotation window should trigger a rotation"
|
|
);
|
|
await rotateSpy.returnValues[0];
|
|
|
|
Assert.notEqual(
|
|
IPPProxyManager.isolationKey,
|
|
savedKey,
|
|
"Rotation produces a new isolation key"
|
|
);
|
|
|
|
await IPPProxyManager.stop();
|
|
IPProtectionService.uninit();
|
|
sandbox.restore();
|
|
});
|
|
|
|
/**
|
|
* Sleep/wake observers are only registered while the proxy is ACTIVE.
|
|
*/
|
|
add_task(async function test_observers_only_active_while_active() {
|
|
let sandbox = sinon.createSandbox();
|
|
setupStubs({ validProxyPass: true });
|
|
await startActiveProxy();
|
|
|
|
await IPPProxyManager.stop();
|
|
Assert.notEqual(
|
|
IPPProxyManager.state,
|
|
IPPProxyStates.ACTIVE,
|
|
"Proxy is no longer active after stop"
|
|
);
|
|
|
|
const observeSpy = sandbox.spy(IPPLifecycleHelper, "observe");
|
|
Services.obs.notifyObservers(null, SLEEP_TOPIC);
|
|
Services.obs.notifyObservers(null, WAKE_TOPIC);
|
|
|
|
Assert.ok(
|
|
observeSpy.notCalled,
|
|
"The helper stops observing power events once the proxy is inactive"
|
|
);
|
|
|
|
IPProtectionService.uninit();
|
|
sandbox.restore();
|
|
});
|
|
|
|
/**
|
|
* On Android the helper reacts to the app moving to the background/foreground
|
|
* the same way it reacts to sleep/wake on desktop: suspend on background, resume
|
|
* on foreground.
|
|
*/
|
|
add_task(async function test_android_background_foreground_cycle() {
|
|
setupStubs({ validProxyPass: true });
|
|
await startActiveProxy();
|
|
|
|
const channelFilter = IPPProxyManager.channelFilter();
|
|
const savedKey = IPPProxyManager.isolationKey;
|
|
|
|
Services.obs.notifyObservers(null, "application-background");
|
|
Assert.equal(
|
|
channelFilter.proxyInfo,
|
|
null,
|
|
"Channel filter is suspended when the app is backgrounded"
|
|
);
|
|
|
|
Services.obs.notifyObservers(null, "application-foreground");
|
|
Assert.ok(
|
|
channelFilter.proxyInfo,
|
|
"Connection resumed when the app is foregrounded"
|
|
);
|
|
Assert.equal(
|
|
IPPProxyManager.isolationKey,
|
|
savedKey,
|
|
"Resuming reuses the isolation key captured before backgrounding"
|
|
);
|
|
|
|
await IPPProxyManager.stop();
|
|
IPProtectionService.uninit();
|
|
});
|
|
|
|
function channelFilterProxyInfoPresent() {
|
|
return !!IPPProxyManager.channelFilter()?.proxyInfo;
|
|
}
|