Which of the site's addresses a request came to, and what each one does.
There are two homes. lmjtfy.fun is the site: the page, /ask, the
archive's feed and sockets. code.lmjtfy.fun is the code: the clone
routes, the code pages and a front page listing every repository
(clone::Repo::ALL), and nothing else, so no question can be asked there
and no socket opened. The old addresses (www.lmjtfy.fun, the
workers.dev one) lead to the right home for good.
The split is made twice, so neither half can be forgotten. gate decides
what happens to a request before any route sees it: a repository's path
on lmjtfy.fun or an old address is sent to the code host. And fetch
(lib.rs) gives each host its own router, so the routes a host does not
serve are not there to be reached by a path gate did not think of.
Pure: strings and a method in, a decision out, tested natively.
18use axum::http::{Method, StatusCode};
20use crate::clone::Repo;
The site's address, bare.
23pub const HOME: &str = "lmjtfy.fun";
The code's: git clone https://code.lmjtfy.fun/<repo>.git.
25pub const CODE: &str = "code.lmjtfy.fun";
The addresses that lead to HOME: where the site was first served, and
the same name with www.
28const ELSEWHERE: [&str; 2] = ["lmjtfy.deizel.workers.dev", "www.lmjtfy.fun"];
Whether the old addresses send the code to the code host: the Worker var
GIT_REDIRECT (wrangler.toml).
A closed type, so a half-way state cannot be written: Off is how the
site was before the code host (the apex serves the clones itself, the
code host serves them too, and no page offers an address that is not yet
known to work); On is the end state. One deploy ships the code host
with Off; once it is checked, a second turns On (README, "Rolling out").
A GIT_REDIRECT that is neither on nor off.
The var's name.
58 pub const VAR: &'static str = "GIT_REDIRECT";
Unset is Off. Anything but exactly on or off is refused, not
guessed at ("true", "ON", " on" and "" included): the caller says so
loudly and runs as Off (resolve).
The switch to run with, and the complaint to log if the var was
refused. Off is the fallback because it is the behaviour that
cannot break a working clone: it never sends anyone to an address
that may not be live, and a mistyped var then fails safe, visibly
in the logs, rather than redirecting every clone.
lmjtfy.fun.
92 Home,
code.lmjtfy.fun.
94 Code,
An old address, which leads to the right home.
96 Elsewhere,
Anything else: staging, a dev server. It serves everything, git included, and redirects nothing, so a change can be tried where it is.
Whether this address serves the repositories itself. The code host and a dev server or staging always do; the site's own addresses only until the redirect is on.
Whether this address serves the repositories' release files
(release.rs). Only the code host, and a dev server or staging, which
have no code host of their own. Never the site's addresses, whatever
the redirect: downloads are not a thing the site did before the code
host existed, so there is nothing to keep working there.
Where a clone is made from, for the commands pages offer: the code host for the site's own addresses once the redirect is on (it is then known to work), and wherever the page is until then, and for a staging or dev server, which serves the clone itself. So a page never advertises an address that has not been checked.
Where the site's front page is, for a link from a page that may be on the code host.
Whether pages here take part in what the site does live: the online
count and toasts over /live, and the report of a page to /seen.
The code host has neither route.
A request, as far as gate reads it.
The path and the query, as sent: /lmjtfy.git/info/refs?service=git-upload-pack.
166 pub target: &'a str,
A WebSocket upgrade.
168 pub socket: bool,
A request a page made for itself (Sec-Fetch-Mode other than
navigate), rather than a person or git following a link.
Route it.
178 Pass,
Send it on for good.
180 Moved { to: String, status: StatusCode },
Answer 405 with this: a method that was never served at the address
the repository left.
The repository a path is under (/lmjtfy.git, /lmjtfy.git/info/refs),
if it is one that is served. Only the exact name counts: /lmjtfy.gitx
and /other.git are not.
193pub fn gate(asked: &Asked<'_>, redirect: GitRedirect) -> Gate { 194 let host = Host::of(asked.host); 195 if matches!(host, Host::Code | Host::Other) { 196 return Gate::Pass; 197 } 198 let path = asked.target.split_once('?').map_or(asked.target, |(path, _)| path); 199 let get = matches!(*asked.method, Method::GET | Method::HEAD); 200 // The code left for its own address. Git follows the redirect of its 201 // first request, `GET info/refs?service=…`, and makes every request after 202 // it, the `POST`s of the pack, at the address it was sent to. So a 203 // `POST` that arrives here is not git following the redirect; it is 204 // something that was never told, and is told. `308`, not `301`: the 205 // method and the path are kept, and a client that ever did send a `POST` 206 // would send it again to the new address. 207 // With the redirect off, the code is not moved: the old addresses do what 208 // they did before the code host existed, below. 209 // The release files (`/whiskers/latest/...`) follow the clone: with the 210 // redirect on, a `GET` is sent to the code host, and anything else is 211 // told. With it off they are not served here at all (`serves_downloads`), 212 // and the path is a `404`. 213 if redirect.is_on() && crate::release::under(path).is_some() { 214 return if get { 215 Gate::Moved { to: format!("https://{CODE}{}", asked.target), status: StatusCode::PERMANENT_REDIRECT } 216 } else { 217 Gate::Refused(format!("The downloads are at https://{CODE}, not here: https://{CODE}{path}")) 218 }; 219 } 220 if redirect.is_on() && repository(path).is_some() { 221 return if get { 222 Gate::Moved { to: format!("https://{CODE}{}", asked.target), status: StatusCode::PERMANENT_REDIRECT } 223 } else { 224 Gate::Refused(format!("The code is at https://{CODE}, not here: git clone https://{CODE}{path}")) 225 }; 226 } 227 // The old addresses of the site itself. A page still open there is left 228 // to finish there: its socket and its own requests would only break if 229 // sent on, and it moves the next time it is loaded. 230 if host == Host::Elsewhere && get && !asked.socket && !asked.own { 231 return Gate::Moved { to: format!("https://{HOME}{}", asked.target), status: StatusCode::MOVED_PERMANENTLY }; 232 } 233 Gate::Pass 234} 235 236#[cfg(test)] 237mod tests { 238 use super::*; 239 240 fn ask<'a>(host: &'a str, method: &'a Method, target: &'a str) -> Asked<'a> { 241 Asked { host, method, target, socket: false, own: false } 242 } 243 244 use GitRedirect::{Off, On}; 245 246 const OLD: [&str; 3] = [HOME, "www.lmjtfy.fun", "lmjtfy.deizel.workers.dev"]; 247 248 #[test] 249 fn an_address_is_told_from_the_others() { 250 assert_eq!(Host::of("lmjtfy.fun"), Host::Home); 251 assert_eq!(Host::of("LMJTFY.fun"), Host::Home); 252 assert_eq!(Host::of("code.lmjtfy.fun"), Host::Code); 253 assert_eq!(Host::of("www.lmjtfy.fun"), Host::Elsewhere); 254 assert_eq!(Host::of("lmjtfy.deizel.workers.dev"), Host::Elsewhere); 255 assert_eq!(Host::of("staging.lmjtfy.fun"), Host::Other); 256 assert_eq!(Host::of("localhost:8787"), Host::Other); 257 // Not a suffix match: a name that merely ends in ours is no one's. 258 assert_eq!(Host::of("evil-code.lmjtfy.fun"), Host::Other); 259 assert_eq!(Host::of("code.lmjtfy.fun.evil.example"), Host::Other); 260 assert_eq!(Host::of(""), Host::Other); 261 } 262 263 #[test] 264 fn only_the_code_host_lacks_the_live_routes() { 265 assert!(!Host::Code.is_live()); 266 for host in [Host::Home, Host::Elsewhere, Host::Other] { 267 assert!(host.is_live()); 268 } 269 assert_eq!(Host::Code.home_link(), "https://lmjtfy.fun/"); 270 assert_eq!(Host::Home.home_link(), "/"); 271 } 272 273 #[test] 274 fn clones_are_offered_from_the_code_host_only_once_it_is_on() { 275 assert_eq!(Host::Home.code_origin("https://lmjtfy.fun", On), "https://code.lmjtfy.fun"); 276 assert_eq!(Host::Elsewhere.code_origin("https://www.lmjtfy.fun", On), "https://code.lmjtfy.fun"); 277 assert_eq!(Host::Code.code_origin("https://code.lmjtfy.fun", On), "https://code.lmjtfy.fun"); 278 assert_eq!(Host::Other.code_origin("http://localhost:8787", On), "http://localhost:8787"); 279 // Off: nothing is advertised that has not been checked; the page's 280 // own address serves the clone. 281 assert_eq!(Host::Home.code_origin("https://lmjtfy.fun", Off), "https://lmjtfy.fun"); 282 assert_eq!(Host::Elsewhere.code_origin("https://www.lmjtfy.fun", Off), "https://www.lmjtfy.fun"); 283 assert_eq!(Host::Code.code_origin("https://code.lmjtfy.fun", Off), "https://code.lmjtfy.fun"); 284 assert_eq!(Host::Other.code_origin("http://localhost:8787", Off), "http://localhost:8787"); 285 } 286 287 #[test] 288 fn the_switch_is_parsed_strictly_and_defaults_off() { 289 assert_eq!(GitRedirect::default(), Off); 290 assert_eq!(GitRedirect::parse(None), Ok(Off)); 291 assert_eq!(GitRedirect::parse(Some("off")), Ok(Off)); 292 assert_eq!(GitRedirect::parse(Some("on")), Ok(On)); 293 for bad in ["", "ON", "Off", "true", "1", " on", "on ", "yes", "enabled"] { 294 assert_eq!(GitRedirect::parse(Some(bad)), Err(BadSwitch(bad.to_owned())), "{bad:?}"); 295 } 296 }
A var that is wrong runs as off, and says so; one that is right says nothing.
300 #[test] 301 fn a_refused_value_falls_back_to_off_with_a_complaint() { 302 assert_eq!(GitRedirect::resolve(None), (Off, None)); 303 assert_eq!(GitRedirect::resolve(Some("on")), (On, None)); 304 let (switch, complaint) = GitRedirect::resolve(Some("ON")); 305 assert_eq!(switch, Off); 306 let complaint = complaint.expect("a complaint"); 307 assert!(complaint.contains("GIT_REDIRECT") && complaint.contains("\"ON\"") && complaint.contains("off"), "{complaint}"); 308 }
310 #[test] 311 fn who_serves_the_repositories_in_each_mode() { 312 for (host, off, on) in [(Host::Home, true, false), (Host::Elsewhere, true, false), (Host::Code, true, true), (Host::Other, true, true)] { 313 assert_eq!(host.serves_git(Off), off, "{host:?} off"); 314 assert_eq!(host.serves_git(On), on, "{host:?} on"); 315 } 316 }
Off is the site as it was before the code host: nothing under a repository's name is moved or refused at the apex, www and workers.dev get the 301 of everything to the apex (git follows it), and the code host passes everything.
322 #[test] 323 fn with_the_redirect_off_the_old_addresses_do_what_they_did_before() { 324 let refs = "/lmjtfy.git/info/refs?service=git-upload-pack"; 325 for method in [Method::GET, Method::HEAD, Method::POST] { 326 assert_eq!(gate(&ask(HOME, &method, refs), Off), Gate::Pass, "{method}"); 327 assert_eq!(gate(&ask(HOME, &method, "/lmjtfy.git/git-upload-pack"), Off), Gate::Pass, "{method}"); 328 } 329 for host in ["www.lmjtfy.fun", "lmjtfy.deizel.workers.dev"] { 330 assert_eq!( 331 gate(&ask(host, &Method::GET, refs), Off), 332 Gate::Moved { to: format!("https://lmjtfy.fun{refs}"), status: StatusCode::MOVED_PERMANENTLY }, 333 "{host}" 334 ); 335 assert_eq!(gate(&ask(host, &Method::POST, "/lmjtfy.git/git-upload-pack"), Off), Gate::Pass, "{host}"); 336 } 337 for target in [refs, "/", "/lmjtfy.git"] { 338 assert_eq!(gate(&ask(CODE, &Method::GET, target), Off), Gate::Pass); 339 } 340 }
Everything that is not about the repositories is the same in both modes.
343 #[test] 344 fn the_modes_differ_only_in_what_is_about_a_repository() { 345 for host in OLD.into_iter().chain([CODE, "staging.lmjtfy.fun"]) { 346 for target in ["/", "/rules", "/ask", "/feed?ms=1", "/nosuchrepo.git/info/refs"] { 347 for method in [Method::GET, Method::POST] { 348 for (socket, own) in [(false, false), (true, false), (false, true)] { 349 let asked = Asked { host, method: &method, target, socket, own }; 350 assert_eq!(gate(&asked, Off), gate(&asked, On), "{method} {host}{target}"); 351 } 352 } 353 } 354 } 355 }
Every repository served, at every address that is not the code host,
with and without a query and a trailing path: the same path and query
at code.lmjtfy.fun, with a 308.
360 #[test] 361 fn every_repository_leads_to_the_code_host_with_its_path_and_query() { 362 let suffixes = [ 363 "", 364 "/", 365 "/info/refs?service=git-upload-pack", 366 "/info/refs", 367 "/git-upload-pack", 368 "/apps/lmjtfy/src/lib.rs", 369 "/apps/lmjtfy/src/lib.rs?raw", 370 "/README.md?view=agents&x=a%20b", 371 "?view=agents", 372 ]; 373 for host in OLD { 374 for repo in Repo::ALL { 375 for suffix in suffixes { 376 for method in [Method::GET, Method::HEAD] { 377 let target = format!("/{}{suffix}", repo.served()); 378 assert_eq!( 379 gate(&ask(host, &method, &target), On), 380 Gate::Moved { to: format!("https://code.lmjtfy.fun{target}"), status: StatusCode::PERMANENT_REDIRECT }, 381 "{method} {host}{target}" 382 ); 383 } 384 } 385 } 386 } 387 }
git's own first request, which it follows, is among them, and it does not matter what else the client sent: git sends neither header the site's own pages do.
392 #[test] 393 fn gits_first_request_is_followed_to_the_same_suffix() { 394 let target = "/lmjtfy.git/info/refs?service=git-upload-pack"; 395 let mut asked = ask(HOME, &Method::GET, target); 396 asked.own = true; 397 asked.socket = true; 398 assert_eq!( 399 gate(&asked, On), 400 Gate::Moved { to: format!("https://code.lmjtfy.fun{target}"), status: StatusCode::PERMANENT_REDIRECT } 401 ); 402 }
A POST is not redirected: a client that sends one at the old address
was not following a redirect of its first request (git sends the rest
to the address that redirect gave it), and it is told where to go.
407 #[test] 408 fn a_post_to_the_old_address_is_told_where_the_code_is() { 409 for host in OLD { 410 for method in [Method::POST, Method::PUT, Method::DELETE, Method::PATCH] { 411 let got = gate(&ask(host, &method, "/lmjtfy.git/git-upload-pack"), On); 412 let Gate::Refused(why) = got else { panic!("{method} {host}: {got:?}") }; 413 assert!(why.contains("https://code.lmjtfy.fun/lmjtfy.git/git-upload-pack"), "{why}"); 414 } 415 } 416 }
418 #[test] 419 fn what_is_not_a_served_repository_is_not_sent_on() { 420 for target in ["/nosuchrepo.git/info/refs?service=git-upload-pack", "/lmjtfy.gitx", "/lmjtfy", "/x/lmjtfy.git", "/lmjtfy.git.evil/a", "//lmjtfy.git"] { 421 assert_eq!(gate(&ask(HOME, &Method::GET, target), On), Gate::Pass, "{target}"); 422 } 423 } 424 425 #[test] 426 fn the_code_host_and_a_dev_server_are_not_redirected() { 427 for host in ["code.lmjtfy.fun", "staging.lmjtfy.fun", "localhost:8787", "127.0.0.1:8787"] { 428 for target in ["/lmjtfy.git/info/refs?service=git-upload-pack", "/", "/rules"] { 429 for method in [Method::GET, Method::POST] { 430 assert_eq!(gate(&ask(host, &method, target), On), Gate::Pass, "{method} {host}{target}"); 431 } 432 } 433 } 434 }
What www and the workers.dev address did before the code host
existed, kept: a permanent redirect of everything else to the bare
address, 301, except what a page still open there is making.
439 #[test] 440 fn the_old_addresses_still_lead_to_the_site_for_good() { 441 let moved = |host, method: &Method, target, socket, own| { 442 gate(&Asked { host, method, target, socket, own }, On) 443 }; 444 let site = |target: &str| Gate::Moved { to: format!("https://lmjtfy.fun{target}"), status: StatusCode::MOVED_PERMANENTLY }; 445 assert_eq!(moved("www.lmjtfy.fun", &Method::GET, "/rules", false, false), site("/rules")); 446 assert_eq!(moved("lmjtfy.deizel.workers.dev", &Method::GET, "/?q=is+it%3F", false, false), site("/?q=is+it%3F")); 447 assert_eq!(moved("www.lmjtfy.fun", &Method::HEAD, "/", false, true), Gate::Pass); 448 assert_eq!(moved("www.lmjtfy.fun", &Method::POST, "/ask", false, false), Gate::Pass); 449 assert_eq!(moved("www.lmjtfy.fun", &Method::GET, "/live", true, false), Gate::Pass); 450 assert_eq!(moved("www.lmjtfy.fun", &Method::GET, "/feed?ms=1&q=x", false, true), Gate::Pass); 451 // The apex serves the site: nothing to move. 452 assert_eq!(moved(HOME, &Method::GET, "/", false, false), Gate::Pass); 453 assert_eq!(moved(HOME, &Method::GET, "/rules?a=b", false, false), Gate::Pass); 454 }
456 #[test] 457 fn a_repository_is_found_in_a_path_only_by_its_first_segment() { 458 assert_eq!(repository("/jevcrates.git"), Some(Repo::Jevcrates)); 459 assert_eq!(repository("/jevcrates.git/info/refs"), Some(Repo::Jevcrates)); 460 assert_eq!(repository("/"), None); 461 assert_eq!(repository(""), None); 462 assert_eq!(repository("/a/jevcrates.git"), None); 463 } 464 465 #[test] 466 fn release_files_are_the_code_hosts_alone() { 467 for host in [Host::Home, Host::Elsewhere] { 468 assert!(!host.serves_downloads()); 469 } 470 for host in [Host::Code, Host::Other] { 471 assert!(host.serves_downloads()); 472 } 473 // Like a clone: sent to the code host once the redirect is on, by a 474 // 308 that keeps the path and the query, from every old address. 475 for host in OLD { 476 let moved = gate(&ask(host, &Method::GET, "/whiskers/latest/arm64.apk"), On); 477 assert_eq!(moved, Gate::Moved { to: format!("https://{CODE}/whiskers/latest/arm64.apk"), status: StatusCode::PERMANENT_REDIRECT }, "{host}"); 478 assert!(matches!(gate(&ask(host, &Method::HEAD, "/whiskers/releases/1.0/a.apk"), On), Gate::Moved { .. }), "{host}"); 479 assert!(matches!(gate(&ask(host, &Method::POST, "/whiskers/latest/arm64.apk"), On), Gate::Refused(_)), "{host}"); 480 // A path that is not a repository's is not the code's. 481 let other = gate(&ask(host, &Method::GET, "/nixos-config/latest/x"), On); 482 assert!(!matches!(other, Gate::Moved { status: StatusCode::PERMANENT_REDIRECT, .. } | Gate::Refused(_)), "{host}: {other:?}"); 483 } 484 // With it off the apex does not serve them (no route) and moves nothing. 485 assert_eq!(gate(&ask(HOME, &Method::GET, "/whiskers/latest/arm64.apk"), Off), Gate::Pass); 486 // Where they are served, nothing is moved. 487 for host in [CODE, "staging.lmjtfy.fun", "localhost:8787"] { 488 assert_eq!(gate(&ask(host, &Method::GET, "/whiskers/latest/arm64.apk"), On), Gate::Pass, "{host}"); 489 } 490 } 491}