Which address a request came to, and what to do with it before any route sees it.
A site has one canonical address and may have old ones that lead to it. A person or a link
that arrives at an old address is sent on for good, with the path and the query kept. A page that
is still open there is left to finish: its socket and its own requests (Sec-Fetch-Mode other
than navigate) would only break if sent on, and it moves the next time it is loaded. Any other
address (staging, a dev server) is served and redirects nothing, so a change can be tried where it
is.
Pure: strings in, a decision out. A site with more homes than one (lmjtfy's code host) puts its own
decisions in front of [Canonical::gate] and keeps this for the old addresses.
A permanent redirect. 308 keeps the method; 301 is what a page's GET has always been given.
The cache-control of a redirect: long enough to spare the old address its traffic, short enough
that a mistake is not stuck in caches.
31pub const MOVED_CACHE_CONTROL: &str = "public, max-age=86400";
Which address a request's Host is.
The canonical address.
37 Canonical,
An address that leads to the canonical one.
39 Old,
This machine: a dev server, which redirects nothing.
41 Local,
GET or HEAD: a request that can be repeated at another address.
50 pub is_read: bool,
The path and the query, as sent.
52 pub target: &'a str,
A WebSocket upgrade.
54 pub socket: bool,
A request a page made for itself, rather than a person or a program following a link.
Route it.
63 Pass,
Send it on for good.
65 Moved { to: String, status: Status },
Answer 405 with this: a method that was never served at the address the thing left.
What happens at an old address to a request that cannot be repeated somewhere else (a POST),
when it is not a page finishing where it started.
Route it here.
75 Pass,
Say where the site is (405): a redirect would repeat it somewhere it was never meant to go.
These, and no others.
84 Listed(&'static [&'static str]),
Every address that is neither the canonical one nor this machine's, so a name nobody thought of cannot serve a second copy of the page (and no account's name need be written down).
A site's canonical address and the old ones.
An origin that is not scheme://host[:port].
A canonical https://host and a listed set of old addresses, all known at compile time. Hosts
are bare names, lower case, no scheme, no port. A name that is also its own old address, or is
not lower case, stops the site being built.
114 pub const fn new(host: &'static str, old: &'static [&'static str]) -> Canonical { 115 assert!(is_bare(host), "a host is a bare lower-case name"); 116 let mut i = 0; 117 while i < old.len() { 118 assert!(is_bare(old[i]), "a host is a bare lower-case name"); 119 assert!(!same(old[i], host), "an address is not its own old address"); 120 i += 1; 121 } 122 Canonical { secure: true, host: std::borrow::Cow::Borrowed(host), old: Old::Listed(old) } 123 }
The canonical address is a runtime value (a deploy's SITE_ORIGIN), and every address that is
neither it nor this machine's leads to it.
127 pub fn every_other_leads_to(origin: &str) -> Result<Canonical, NotAnOrigin> { 128 let bad = || NotAnOrigin(origin.to_owned()); 129 let (scheme, host) = origin.split_once("://").ok_or_else(bad)?; 130 let secure = match scheme { 131 "https" => true, 132 "http" => false, 133 _ => return Err(bad()), 134 }; 135 if host.is_empty() || host.contains(['/', '?', '#', ' ']) { 136 return Err(bad()); 137 } 138 Ok(Canonical { secure, host: std::borrow::Cow::Owned(host.to_ascii_lowercase()), old: Old::EveryOther }) 139 }
https://host, as a link to the canonical address is written.
Exact match, case-insensitive, never a suffix: a name that merely ends in ours is no one's.
151 pub fn of(&self, host: &str) -> Where { 152 let host = host.trim().to_ascii_lowercase(); 153 if host == self.host { 154 return Where::Canonical; 155 } 156 if is_local(&host) { 157 return Where::Local; 158 } 159 match self.old { 160 Old::EveryOther => Where::Old, 161 Old::Listed(list) if list.contains(&host.as_str()) => Where::Old, 162 Old::Listed(_) => Where::Other, 163 } 164 }
An old address leads here, for a request that is not a socket and not a page finishing where it
started. A read is moved; anything else is unrepeatable.
168 pub fn gate(&self, asked: &Asked<'_>, status: Status, unrepeatable: Unrepeatable) -> Gate { 169 if self.of(asked.host) != Where::Old || asked.socket || asked.own { 170 return Gate::Pass; 171 } 172 if asked.is_read { 173 return Gate::Moved { to: format!("{}{}", self.origin(), asked.target), status }; 174 } 175 match unrepeatable { 176 Unrepeatable::Pass => Gate::Pass, 177 Unrepeatable::Refuse => { 178 let path = asked.target.split('?').next().unwrap_or("/"); 179 Gate::Refused(format!("The site is at {}, not here: use {}{path}", self.origin(), self.origin())) 180 } 181 } 182 } 183}
A host with its port taken off (an IPv6 literal keeps its brackets).
This machine's own address.
199const fn same(a: &str, b: &str) -> bool { 200 let (a, b) = (a.as_bytes(), b.as_bytes()); 201 if a.len() != b.len() { 202 return false; 203 } 204 let mut i = 0; 205 while i < a.len() { 206 if a[i] != b[i] { 207 return false; 208 } 209 i += 1; 210 } 211 true 212} 213 214const fn is_bare(host: &str) -> bool { 215 let host = host.as_bytes(); 216 if host.is_empty() { 217 return false; 218 } 219 let mut i = 0; 220 while i < host.len() { 221 let b = host[i]; 222 if !(b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'.' || b == b'-') { 223 return false; 224 } 225 i += 1; 226 } 227 true 228}
A switch a deploy sets in the environment. Exactly on or off; unset is off. Anything else is
refused, not guessed at (true, ON, on and the empty string included), so a mistyped value is
loud and the site runs as off.
A switch whose value is neither on nor off.
247impl std::fmt::Display for BadSwitch { 248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 249 write!(f, "{} is {:?}; it must be exactly \"on\" or \"off\". Treating it as \"off\".", self.var, self.value) 250 } 251} 252 253impl std::error::Error for BadSwitch {} 254 255impl Switch { 256 pub fn parse(var: &str, value: Option<&str>) -> Result<Switch, BadSwitch> { 257 match value { 258 None | Some("off") => Ok(Switch::Off), 259 Some("on") => Ok(Switch::On), 260 Some(other) => Err(BadSwitch { var: var.to_owned(), value: other.to_owned() }), 261 } 262 }
The switch to run with, and the complaint to log if the value was refused. Off is the
fallback because it is the behaviour that cannot break a working address.
273 pub fn is_on(self) -> bool { 274 self == Switch::On 275 } 276} 277 278#[cfg(test)] 279mod tests { 280 use super::*; 281 282 const SITE: Canonical = Canonical::new("example.fun", &["www.example.fun", "example.workers.dev"]); 283 const MOVE: Status = Status::PermanentRedirect; 284 285 fn ask<'a>(host: &'a str, target: &'a str) -> Asked<'a> { 286 Asked { host, is_read: true, target, socket: false, own: false } 287 } 288 289 fn moved(to: &str) -> Gate { 290 Gate::Moved { to: to.into(), status: MOVE } 291 } 292 293 #[test] 294 fn an_address_is_told_from_the_others() { 295 assert_eq!(SITE.of("example.fun"), Where::Canonical); 296 assert_eq!(SITE.of("EXAMPLE.fun"), Where::Canonical); 297 assert_eq!(SITE.of("www.example.fun"), Where::Old); 298 assert_eq!(SITE.of("staging.example.fun"), Where::Other); 299 assert_eq!(SITE.of("localhost:8787"), Where::Local); 300 assert_eq!(SITE.of("evil-www.example.fun"), Where::Other); 301 assert_eq!(SITE.of("example.fun.evil.example"), Where::Other); 302 assert_eq!(SITE.of("localhost.evil.example"), Where::Other); 303 assert_eq!(SITE.of(""), Where::Other); 304 } 305 306 #[test] 307 fn an_old_address_sends_a_read_on_with_the_path_and_query() { 308 assert_eq!(SITE.gate(&ask("www.example.fun", "/q/a?x=1"), MOVE, Unrepeatable::Pass), moved("https://example.fun/q/a?x=1")); 309 assert_eq!(Status::MovedPermanently.code(), 301); 310 assert_eq!(Status::PermanentRedirect.code(), 308); 311 } 312 313 #[test] 314 fn a_page_still_open_there_and_every_other_address_is_left_alone() { 315 let pass = |a: Asked| SITE.gate(&a, MOVE, Unrepeatable::Refuse); 316 assert_eq!(pass(Asked { socket: true, ..ask("www.example.fun", "/live") }), Gate::Pass); 317 assert_eq!(pass(Asked { own: true, is_read: false, ..ask("www.example.fun", "/mood") }), Gate::Pass); 318 assert_eq!(pass(ask("example.fun", "/")), Gate::Pass); 319 assert_eq!(pass(ask("staging.example.fun", "/")), Gate::Pass); 320 assert_eq!(pass(ask("localhost:8789", "/")), Gate::Pass); 321 } 322 323 #[test] 324 fn what_cannot_be_repeated_is_passed_or_told_where_the_site_is() { 325 let post = Asked { is_read: false, ..ask("www.example.fun", "/mood?x=1") }; 326 assert_eq!(SITE.gate(&post, MOVE, Unrepeatable::Pass), Gate::Pass); 327 let Gate::Refused(why) = SITE.gate(&post, MOVE, Unrepeatable::Refuse) else { panic!() }; 328 assert!(why.contains("https://example.fun/mood") && !why.contains("x=1"), "a query is not echoed: {why}"); 329 } 330 331 #[test] 332 fn a_runtime_origin_sends_every_other_address_on() { 333 let site = Canonical::every_other_leads_to("https://Site.Example").unwrap(); 334 assert_eq!(site.host(), "site.example"); 335 assert_eq!(site.of("Site.example"), Where::Canonical); 336 assert_eq!(site.of("anything.workers.dev"), Where::Old); 337 assert_eq!(site.of("[::1]:8789"), Where::Local); 338 assert_eq!(site.of("127.0.0.1:8789"), Where::Local); 339 assert_eq!(site.of("app.localhost:1"), Where::Local); 340 assert_eq!(site.of("site.example.evil.example"), Where::Old); 341 assert_eq!(site.gate(&ask("anything.workers.dev", "/?a=b"), MOVE, Unrepeatable::Refuse), moved("https://site.example/?a=b")); 342 // A dev server's own origin is canonical at its own port, and another port of it is local. 343 let dev = Canonical::every_other_leads_to("http://localhost:8788").unwrap(); 344 assert_eq!(dev.of("localhost:8788"), Where::Canonical); 345 assert_eq!(dev.of("localhost:9"), Where::Local); 346 assert_eq!(dev.origin(), "http://localhost:8788"); 347 } 348 349 #[test] 350 fn an_origin_must_be_scheme_and_host_and_nothing_after() { 351 for bad in ["site.example", "ftp://site.example", "https://", "https://a.example/path", "https://a.example?x", "https://a b"] { 352 assert_eq!(Canonical::every_other_leads_to(bad).unwrap_err(), NotAnOrigin(bad.into()), "{bad}"); 353 } 354 } 355 356 #[test] 357 fn a_switch_is_exactly_on_or_off() { 358 assert_eq!(Switch::parse("V", None), Ok(Switch::Off)); 359 assert_eq!(Switch::parse("V", Some("on")), Ok(Switch::On)); 360 for bad in ["true", "ON", " on", "", "1"] { 361 assert!(Switch::parse("V", Some(bad)).is_err(), "{bad:?}"); 362 } 363 let (switch, complaint) = Switch::resolve("GIT_REDIRECT", Some("yes")); 364 assert_eq!(switch, Switch::Off); 365 assert!(complaint.unwrap().contains("GIT_REDIRECT")); 366 assert_eq!(Switch::resolve("V", Some("on")), (Switch::On, None)); 367 } 368 369 #[test] 370 #[should_panic(expected = "bare lower-case")] 371 fn a_host_with_a_scheme_is_refused() { 372 Canonical::new("https://example.fun", &[]); 373 } 374 375 #[test] 376 #[should_panic(expected = "its own old address")] 377 fn an_address_that_is_its_own_old_address_is_refused() { 378 Canonical::new("example.fun", &["example.fun"]); 379 } 380}