| 1747 |
1747 |
|
/// is the `config_check_env_file` guard's job, on the node, against that node's
|
| 1748 |
1748 |
|
/// real env file.
|
| 1749 |
1749 |
|
fn code_smoke_env(cmd: &mut tokio::process::Command, ctx: &GateCtx, db_url: &str) {
|
| 1750 |
|
- |
let origin = format!("http://127.0.0.1:{}", ctx.cfg.code_smoke_port);
|
|
1750 |
+ |
// `localhost`, not `127.0.0.1`, and the distinction is load-bearing. The
|
|
1751 |
+ |
// server derives its WebAuthn relying-party id from HOST_URL's host, and
|
|
1752 |
+ |
// `WebauthnBuilder::new` validates that id against `Url::domain()` — which
|
|
1753 |
+ |
// is `None` for an IP literal, so an origin of `http://127.0.0.1:<port>`
|
|
1754 |
+ |
// fails with WebauthnError::Configuration before the server ever binds. It
|
|
1755 |
+ |
// is a real ceiling on the gate, not a preference: no IP-literal origin can
|
|
1756 |
+ |
// boot this binary. `localhost` is a domain, and matches the derived rp_id.
|
|
1757 |
+ |
//
|
|
1758 |
+ |
// HOST stays 127.0.0.1: that is the bind address, and the gate probes the
|
|
1759 |
+ |
// loopback address directly, so only the advertised origin changes.
|
|
1760 |
+ |
let origin = format!("http://localhost:{}", ctx.cfg.code_smoke_port);
|
| 1751 |
1761 |
|
cmd.env("DATABASE_URL", db_url)
|
| 1752 |
1762 |
|
.env("HOST", "127.0.0.1")
|
| 1753 |
1763 |
|
.env("PORT", ctx.cfg.code_smoke_port.to_string())
|
| 2411 |
2421 |
|
}
|
| 2412 |
2422 |
|
}
|
| 2413 |
2423 |
|
|
|
2424 |
+ |
/// True when the URL's host parses as a domain rather than an IP literal,
|
|
2425 |
+ |
/// which is the distinction `Url::domain()` draws and WebAuthn depends on.
|
|
2426 |
+ |
fn url_host_is_a_domain(url: &str) -> bool {
|
|
2427 |
+ |
let after = url.split("://").nth(1).unwrap_or("");
|
|
2428 |
+ |
let host = after.split(['/', '?', '#']).next().unwrap_or("");
|
|
2429 |
+ |
let host = host.rsplit('@').next().unwrap_or(host);
|
|
2430 |
+ |
let host = if let Some(rest) = host.strip_prefix('[') {
|
|
2431 |
+ |
rest.split(']').next().unwrap_or("")
|
|
2432 |
+ |
} else {
|
|
2433 |
+ |
host.split(':').next().unwrap_or("")
|
|
2434 |
+ |
};
|
|
2435 |
+ |
!host.is_empty() && host.parse::<std::net::IpAddr>().is_err()
|
|
2436 |
+ |
}
|
|
2437 |
+ |
|
| 2414 |
2438 |
|
fn resolving_ctx(worktree: &str, aux: &[(&str, &str)]) -> GateCtx {
|
| 2415 |
2439 |
|
GateCtx {
|
| 2416 |
2440 |
|
pool: SqlitePool::connect_lazy("sqlite::memory:").unwrap(),
|
| 3582 |
3606 |
|
}
|
| 3583 |
3607 |
|
// Loopback, so Config::from_env's is_production branch stays false and
|
| 3584 |
3608 |
|
// the gate never trips MissingPublicBucket for want of an S3 bucket.
|
| 3585 |
|
- |
assert!(set["HOST_URL"].starts_with("http://127.0.0.1"));
|
|
3609 |
+ |
assert!(set["HOST_URL"].starts_with("http://localhost"));
|
|
3610 |
+ |
// Not an IP literal: the server derives its WebAuthn rp_id from this
|
|
3611 |
+ |
// host, and WebauthnBuilder rejects an origin whose Url::domain() is
|
|
3612 |
+ |
// None, which is every IP address. An IP here cannot boot the server.
|
|
3613 |
+ |
assert!(
|
|
3614 |
+ |
url_host_is_a_domain(&set["HOST_URL"]),
|
|
3615 |
+ |
"HOST_URL host must be a domain, not an IP literal: {}",
|
|
3616 |
+ |
set["HOST_URL"],
|
|
3617 |
+ |
);
|
| 3586 |
3618 |
|
assert_eq!(set["HOST"], "127.0.0.1");
|
| 3587 |
3619 |
|
// The signing secret has to clear the server's 32-char floor, or the
|
| 3588 |
3620 |
|
// gate fails with WeakSigningSecret instead of testing anything.
|