Web: a launch behind an expired sign-in shows the sign-in instead of the kept build (#2151)
Thanks @nofuturekid.
This commit is contained in:
1 parent
7a3221d2dd
commit
a11446fd17
2 files changed
+52
-7
No files matched your search
@@ -13,8 +13,10 @@
|
|||||||
// unchanged, the kept build starts; when a new build is deployed, it is downloaded first
|
// unchanged, the kept build starts; when a new build is deployed, it is downloaded first
|
||||||
// (waiting at most UPDATE_TIMEOUT_MS; the download goes on in the background after that) and
|
// (waiting at most UPDATE_TIMEOUT_MS; the download goes on in the background after that) and
|
||||||
// started. When the site has no `build.json` (the app was removed), launches go to the network,
|
// started. When the site has no `build.json` (the app was removed), launches go to the network,
|
||||||
// the kept build answering when that fails. Each page then gets its files from the build it
|
// the kept build answering when that fails. So they do when `build.json` is redirected or
|
||||||
// started (remembered in PAGES), never from another build with a file of the same name.
|
// refused (a sign-in in front of the app): the browser then shows the sign-in. Each page then
|
||||||
|
// gets its files from the build it started (remembered in PAGES), never from another build
|
||||||
|
// with a file of the same name.
|
||||||
//
|
//
|
||||||
// A cache is only deleted when nothing uses it: a download holds its lock (Web Locks, shared,
|
// A cache is only deleted when nothing uses it: a download holds its lock (Web Locks, shared,
|
||||||
// in any tab or worker; the browser releases it when the download ends or its worker stops), and
|
// in any tab or worker; the browser releases it when the download ends or its worker stops), and
|
||||||
@@ -158,9 +160,14 @@ function within(ms, promise) {
|
|||||||
/** No app is deployed here any more (the server says `build.json` doesn't exist). */
|
/** No app is deployed here any more (the server says `build.json` doesn't exist). */
|
||||||
const GONE = "gone";
|
const GONE = "gone";
|
||||||
|
|
||||||
/** The deployed build's version and files, GONE, or null (unknown: offline, failing, invalid). */
|
/** The site wants a sign-in first (`build.json` is redirected, often to another origin, or refused). */
|
||||||
|
const SIGN_IN = "sign-in";
|
||||||
|
|
||||||
|
/** The deployed build's version and files, GONE, SIGN_IN, or null (unknown: offline, failing, invalid). */
|
||||||
async function deployed() {
|
async function deployed() {
|
||||||
const response = await fetch(url("build.json"), { cache: "no-cache" });
|
// `manual`: a redirect to another origin would only fail (CORS), looking like being offline.
|
||||||
|
const response = await fetch(url("build.json"), { cache: "no-cache", redirect: "manual" });
|
||||||
|
if (response.type === "opaqueredirect" || response.status === 401 || response.status === 403) return SIGN_IN;
|
||||||
if (response.status === 404 || response.status === 410) return GONE;
|
if (response.status === 404 || response.status === 410) return GONE;
|
||||||
const build = response.ok ? await response.json() : null;
|
const build = response.ok ? await response.json() : null;
|
||||||
const valid = build && typeof build.version === "string" && Array.isArray(build.files);
|
const valid = build && typeof build.version === "string" && Array.isArray(build.files);
|
||||||
@@ -182,6 +189,7 @@ async function launch(event) {
|
|||||||
if (!kept) return [await fetch(event.request), NETWORK]; // Nothing kept yet (the first visit).
|
if (!kept) return [await fetch(event.request), NETWORK]; // Nothing kept yet (the first visit).
|
||||||
extend(event, sweep());
|
extend(event, sweep());
|
||||||
const build = await within(NAVIGATE_TIMEOUT_MS, deployed());
|
const build = await within(NAVIGATE_TIMEOUT_MS, deployed());
|
||||||
|
if (build === SIGN_IN) return [await fetch(event.request), NETWORK];
|
||||||
if (build === GONE) {
|
if (build === GONE) {
|
||||||
const page = await fetch(event.request).catch(() => null);
|
const page = await fetch(event.request).catch(() => null);
|
||||||
return page && page.ok ? [page, NETWORK] : [kept, CACHE];
|
return page && page.ok ? [page, NETWORK] : [kept, CACHE];
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
//! `/solidcraft/app/` (as on GitHub Pages, with its 10-minute HTTP caching) from a local server
|
//! `/solidcraft/app/` (as on GitHub Pages, with its 10-minute HTTP caching) from a local server
|
||||||
//! that can also act offline, flaky, hung or mid-deployment, plus another copy of the app at
|
//! that can also act offline, flaky, hung or mid-deployment, plus another copy of the app at
|
||||||
//! `/solidcraft/elsewhere/` on the same origin, and drives headless Chrome through the DevTools
|
//! `/solidcraft/elsewhere/` on the same origin, and drives headless Chrome through the DevTools
|
||||||
//! protocol through these steps (twenty-one page launches, which failures are numbered by):
|
//! protocol through these steps (twenty-two page launches, which failures are numbered by):
|
||||||
//!
|
//!
|
||||||
//! 1. online, build v1: the page loads, the service worker takes over and keeps every file;
|
//! 1. online, build v1: the page loads, the service worker takes over and keeps every file;
|
||||||
//! 2. offline: v1 starts from the kept copy;
|
//! 2. offline: v1 starts from the kept copy;
|
||||||
@@ -33,7 +33,9 @@
|
|||||||
//! 17. offline: v6 still starts from the kept copy;
|
//! 17. offline: v6 still starts from the kept copy;
|
||||||
//! 18. a development build (`trunk serve`) on the same origin: it starts (no `build.json`)
|
//! 18. a development build (`trunk serve`) on the same origin: it starts (no `build.json`)
|
||||||
//! while the worker retires, dropping everything kept;
|
//! while the worker retires, dropping everything kept;
|
||||||
//! 19. the development build starts, from the network, with no worker.
|
//! 19. the development build starts, from the network, with no worker;
|
||||||
|
//! 20. a sign-in in front of the app (everything redirects to another origin) while v6 is kept:
|
||||||
|
//! the launch shows the sign-in page, not the kept v6.
|
||||||
//!
|
//!
|
||||||
//! The site is a small stand-in app (default) or a real web build (`--site dist/web`, after
|
//! The site is a small stand-in app (default) or a real web build (`--site dist/web`, after
|
||||||
//! `trunk build --release`); a real build must hold the `sw.js` and `build.json` the generator
|
//! `trunk build --release`); a real build must hold the `sw.js` and `build.json` the generator
|
||||||
@@ -65,6 +67,8 @@ const LAUNCH_TIMEOUT: Duration = Duration::from_secs(90);
|
|||||||
const SLOW_NETWORK_WAIT: Duration = Duration::from_secs(3);
|
const SLOW_NETWORK_WAIT: Duration = Duration::from_secs(3);
|
||||||
/// How long a file takes in Mode::SlowFiles.
|
/// How long a file takes in Mode::SlowFiles.
|
||||||
const SLOW_FILE: Duration = Duration::from_secs(6);
|
const SLOW_FILE: Duration = Duration::from_secs(6);
|
||||||
|
/// Where Mode::SignIn redirects to (on `localhost`: another origin than the 127.0.0.1 app).
|
||||||
|
const SIGN_IN: &str = "/sign-in";
|
||||||
|
|
||||||
/// Exercise the real browser build's command bridge, using the same Chrome harness as the offline test.
|
/// Exercise the real browser build's command bridge, using the same Chrome harness as the offline test.
|
||||||
pub fn run_api(site: &str) -> Result<(), String> {
|
pub fn run_api(site: &str) -> Result<(), String> {
|
||||||
@@ -347,6 +351,16 @@ pub fn run(root: &Path, site: Option<&str>, required: bool) -> Result<(), String
|
|||||||
server.set(Mode::Refuse);
|
server.set(Mode::Refuse);
|
||||||
step("offline: build v6 still starts from the kept copy", launch(&mut cdp, "v6")?);
|
step("offline: build v6 still starts from the kept copy", launch(&mut cdp, "v6")?);
|
||||||
|
|
||||||
|
// In a tab of its own: in the tab that was redirected, Chrome skipped the next launch's check
|
||||||
|
// for a new worker (the development build's step below needs it).
|
||||||
|
server.set(Mode::SignIn);
|
||||||
|
let t = Instant::now();
|
||||||
|
let mut tab = browser.new_tab()?;
|
||||||
|
tab.call("Page.navigate", json!({ "url": format!("http://127.0.0.1:{}{PREFIX}", server.port) }))?;
|
||||||
|
wait_for(&mut tab, "location.pathname", "the sign-in page is shown", |p| p == SIGN_IN)?;
|
||||||
|
tab.call("Page.close", json!({}))?;
|
||||||
|
step("sign-in in front of the app: the launch shows the sign-in, not the kept v6", t.elapsed());
|
||||||
|
|
||||||
server.set(Mode::Serve(dev));
|
server.set(Mode::Serve(dev));
|
||||||
let t = launch(&mut cdp, "dev")?;
|
let t = launch(&mut cdp, "dev")?;
|
||||||
wait_for(&mut cdp, &state_js, "the worker retires", |s| s["registered"] == 0 && s["caches"] == json!({}))?;
|
wait_for(&mut cdp, &state_js, "the worker retires", |s| s["registered"] == 0 && s["caches"] == json!({}))?;
|
||||||
@@ -502,6 +516,9 @@ enum Mode {
|
|||||||
SlowFiles(PathBuf),
|
SlowFiles(PathBuf),
|
||||||
/// Like SlowFiles, but `index.html` fails at once.
|
/// Like SlowFiles, but `index.html` fails at once.
|
||||||
FailFast(PathBuf),
|
FailFast(PathBuf),
|
||||||
|
/// A sign-in in front of the app (an expired session): every request is redirected to
|
||||||
|
/// SIGN_IN on another origin.
|
||||||
|
SignIn,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Server {
|
struct Server {
|
||||||
@@ -563,16 +580,31 @@ fn answer(mut stream: TcpStream, shared: &Mutex<Mode>, slow: &AtomicUsize, elsew
|
|||||||
if reader.read_line(&mut request).is_err() {
|
if reader.read_line(&mut request).is_err() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let mut port = "";
|
||||||
|
let mut host = String::new();
|
||||||
loop {
|
loop {
|
||||||
let mut header = String::new();
|
let mut header = String::new();
|
||||||
match reader.read_line(&mut header) {
|
match reader.read_line(&mut header) {
|
||||||
Ok(0) | Err(_) => return,
|
Ok(0) | Err(_) => return,
|
||||||
Ok(_) if header == "\r\n" => break,
|
Ok(_) if header == "\r\n" => break,
|
||||||
Ok(_) => {}
|
Ok(_) => {
|
||||||
|
if let Some(h) = header.strip_prefix("Host: ") {
|
||||||
|
host = h.trim().to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some((_, p)) = host.rsplit_once(':') {
|
||||||
|
port = p;
|
||||||
|
}
|
||||||
let path = request.split_whitespace().nth(1).unwrap_or("/");
|
let path = request.split_whitespace().nth(1).unwrap_or("/");
|
||||||
let path = path.split('?').next().unwrap_or_default();
|
let path = path.split('?').next().unwrap_or_default();
|
||||||
|
if path == SIGN_IN {
|
||||||
|
let body = b"<!DOCTYPE html><html><body>Sign in</body></html>";
|
||||||
|
let head = format!("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", body.len());
|
||||||
|
let _ = stream.write_all(head.as_bytes()).and_then(|()| stream.write_all(body));
|
||||||
|
return;
|
||||||
|
}
|
||||||
let (file, other) = match path.strip_prefix(ELSEWHERE) {
|
let (file, other) = match path.strip_prefix(ELSEWHERE) {
|
||||||
Some(f) => (Some(f), true),
|
Some(f) => (Some(f), true),
|
||||||
None => (path.strip_prefix(PREFIX), false),
|
None => (path.strip_prefix(PREFIX), false),
|
||||||
@@ -603,6 +635,11 @@ fn answer(mut stream: TcpStream, shared: &Mutex<Mode>, slow: &AtomicUsize, elsew
|
|||||||
(dir, false, false)
|
(dir, false, false)
|
||||||
}
|
}
|
||||||
Mode::Mixed(dir, next) => (if file == Some("index.html") { next } else { dir }, false, false),
|
Mode::Mixed(dir, next) => (if file == Some("index.html") { next } else { dir }, false, false),
|
||||||
|
Mode::SignIn => {
|
||||||
|
let head = format!("HTTP/1.1 302 Found\r\nLocation: http://localhost:{port}{SIGN_IN}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
|
||||||
|
let _ = stream.write_all(head.as_bytes());
|
||||||
|
return;
|
||||||
|
}
|
||||||
Mode::Refuse => return,
|
Mode::Refuse => return,
|
||||||
Mode::Hang => return hold(shared, |m| matches!(m, Mode::Hang)),
|
Mode::Hang => return hold(shared, |m| matches!(m, Mode::Hang)),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user