From d9c2ee6e00f7e3d1c57854c3899b241f95b25f14 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Wed, 11 Jan 2023 15:02:43 -0800 Subject: [PATCH 1/3] Redirect HTTP to HTTPS --- deploy/ord.service | 3 +- src/subcommand/server.rs | 93 ++++++++++++++++++++++++++++++++-------- 2 files changed, 77 insertions(+), 19 deletions(-) diff --git a/deploy/ord.service b/deploy/ord.service index 9dda830b8d..fedbccfb29 100644 --- a/deploy/ord.service +++ b/deploy/ord.service @@ -16,7 +16,8 @@ ExecStart=/usr/local/bin/ord \ server \ --acme-contact mailto:casey@rodarmor.com \ --http \ - --https + --https \ + --redirect-http-to-https Group=ord MemoryDenyWriteExecute=true NoNewPrivileges=true diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index bb858bc620..1eb5b4bcec 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1,10 +1,9 @@ -use super::*; - use { self::{ deserialize_from_str::DeserializeFromStr, error::{OptionExt, ServerError, ServerResult}, }, + super::*, crate::templates::{ BlockHtml, ClockSvg, HomeHtml, InputHtml, InscriptionHtml, InscriptionsHtml, OutputHtml, PageContent, PageHtml, PreviewImageHtml, PreviewTextHtml, PreviewUnknownHtml, RangeHtml, @@ -13,7 +12,7 @@ use { axum::{ body, extract::{Extension, Path, Query}, - http::{header, HeaderMap, HeaderValue, StatusCode}, + http::{header, HeaderMap, HeaderValue, StatusCode, Uri}, response::{IntoResponse, Redirect, Response}, routing::get, Router, @@ -110,6 +109,8 @@ pub(crate) struct Server { http: bool, #[clap(long, help = "Serve HTTPS traffic on .")] https: bool, + #[clap(long, help = "Redirect HTTP traffic to HTTPS.")] + redirect_http_to_https: bool, } impl Server { @@ -198,6 +199,17 @@ impl Server { ); } + let redirect_destination = if self.redirect_http_to_https && https_acceptor.is_none() { + let acme_domain = Self::acme_domains(&self.acme_domain)? + .into_iter() + .nth(0) + .unwrap(); + + Some(format!("https://{acme_domain}")) + } else { + None + }; + Ok(tokio::spawn(async move { if let Some(acceptor) = https_acceptor { axum_server::Server::bind(addr) @@ -205,6 +217,16 @@ impl Server { .acceptor(acceptor) .serve(router.into_make_service()) .await + } else if let Some(redirect_destination) = redirect_destination { + axum_server::Server::bind(addr) + .handle(handle) + .serve( + Router::new() + .fallback(Self::redirect_http_to_https) + .layer(Extension(redirect_destination)) + .into_make_service(), + ) + .await } else { axum_server::Server::bind(addr) .handle(handle) @@ -679,6 +701,17 @@ impl Server { .page(chain, index.has_sat_index()?), ) } + + async fn redirect_http_to_https( + Extension(mut destination): Extension, + uri: Uri, + ) -> Redirect { + if let Some(path_and_query) = uri.path_and_query() { + destination.push_str(path_and_query.as_str()); + } + + Redirect::to(&destination) + } } #[cfg(test)] @@ -696,10 +729,14 @@ mod tests { impl TestServer { fn new() -> Self { - Self::new_with_args(&[]) + Self::new_with_args(&[], &[]) } - fn new_with_args(args: &[&str]) -> Self { + fn new_with_sat_index() -> Self { + Self::new_with_args(&["--index-sats"], &[]) + } + + fn new_with_args(ord_args: &[&str], server_args: &[&str]) -> Self { let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); let tempdir = TempDir::new().unwrap(); @@ -717,12 +754,13 @@ mod tests { let url = Url::parse(&format!("http://127.0.0.1:{port}")).unwrap(); let (options, server) = parse_server_args(&format!( - "ord --chain regtest --rpc-url {} --cookie-file {} --data-dir {} {} server --http-port {} --address 127.0.0.1", + "ord --chain regtest --rpc-url {} --cookie-file {} --data-dir {} {} server --http-port {} --address 127.0.0.1 {}", bitcoin_rpc_server.url(), cookiefile.to_str().unwrap(), tempdir.path().to_str().unwrap(), - args.join(" "), + ord_args.join(" "), port, + server_args.join(" "), )); let index = Arc::new(Index::open(&options).unwrap()); @@ -738,8 +776,13 @@ mod tests { thread::sleep(Duration::from_millis(25)); } + let client = reqwest::blocking::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build() + .unwrap(); + for i in 0.. { - match reqwest::blocking::get(format!("http://127.0.0.1:{port}/status")) { + match client.get(format!("http://127.0.0.1:{port}/status")).send() { Ok(_) => break, Err(err) => { if i == 400 { @@ -1069,6 +1112,20 @@ mod tests { ); } + #[test] + fn http_to_https_redirect_with_path() { + TestServer::new_with_args(&[], &["--redirect-http-to-https"]).assert_redirect( + "/sat/0", + &format!("https://{}/sat/0", sys_info::hostname().unwrap()), + ); + } + + #[test] + fn http_to_https_redirect_with_empty() { + TestServer::new_with_args(&[], &["--redirect-http-to-https"]) + .assert_redirect("/", &format!("https://{}/", sys_info::hostname().unwrap())); + } + #[test] fn status() { TestServer::new().assert_response("/status", StatusCode::OK, "OK"); @@ -1188,7 +1245,7 @@ mod tests { #[test] fn output_with_sat_index() { - TestServer::new_with_args(&["--index-sats"]).assert_response_regex( + TestServer::new_with_sat_index().assert_response_regex( "/output/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0", StatusCode::OK, ".*Output 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0.*

Output 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0

@@ -1401,7 +1458,7 @@ mod tests { #[test] fn rare_with_index() { - TestServer::new_with_args(&["--index-sats"]).assert_response( + TestServer::new_with_sat_index().assert_response( "/rare.txt", StatusCode::OK, "sat\tsatpoint @@ -1412,7 +1469,7 @@ mod tests { #[test] fn rare_without_sat_index() { - TestServer::new_with_args(&[]).assert_response( + TestServer::new().assert_response( "/rare.txt", StatusCode::NOT_FOUND, "tracking rare sats requires index created with `--index-sats` flag", @@ -1421,7 +1478,7 @@ mod tests { #[test] fn show_rare_txt_in_header_with_sat_index() { - TestServer::new_with_args(&["--index-sats"]).assert_response_regex( + TestServer::new_with_sat_index().assert_response_regex( "/", StatusCode::OK, ".* @@ -1433,7 +1490,7 @@ mod tests { #[test] fn rare_sat_location() { - TestServer::new_with_args(&["--index-sats"]).assert_response_regex( + TestServer::new_with_sat_index().assert_response_regex( "/sat/0", StatusCode::OK, ".*>4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0<.*", @@ -1505,7 +1562,7 @@ mod tests { #[test] fn outputs_traversed_are_tracked() { - let server = TestServer::new_with_args(&["--index-sats"]); + let server = TestServer::new_with_sat_index(); assert_eq!( server @@ -1537,7 +1594,7 @@ mod tests { #[test] fn coinbase_sat_ranges_are_tracked() { - let server = TestServer::new_with_args(&["--index-sats"]); + let server = TestServer::new_with_sat_index(); assert_eq!( server.index.statistic(crate::index::Statistic::SatRanges), @@ -1561,7 +1618,7 @@ mod tests { #[test] fn split_sat_ranges_are_tracked() { - let server = TestServer::new_with_args(&["--index-sats"]); + let server = TestServer::new_with_sat_index(); assert_eq!( server.index.statistic(crate::index::Statistic::SatRanges), @@ -1585,7 +1642,7 @@ mod tests { #[test] fn fee_sat_ranges_are_tracked() { - let server = TestServer::new_with_args(&["--index-sats"]); + let server = TestServer::new_with_sat_index(); assert_eq!( server.index.statistic(crate::index::Statistic::SatRanges), @@ -1749,7 +1806,7 @@ mod tests { #[test] fn inscription_page_has_sat_when_sats_are_tracked() { - let server = TestServer::new_with_args(&["--index-sats"]); + let server = TestServer::new_with_sat_index(); server.mine_blocks(1); let inscription_id = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { From 2a7987f74f74e853bfb2478f6449e5960be4efed Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Wed, 11 Jan 2023 15:22:55 -0800 Subject: [PATCH 2/3] Add spawn config --- src/subcommand/server.rs | 113 ++++++++++++++++++++++++--------------- 1 file changed, 70 insertions(+), 43 deletions(-) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 1eb5b4bcec..39c725ad4d 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -51,6 +51,12 @@ impl FromStr for BlockQuery { } } +enum SpawnConfig { + Https(AxumAcceptor), + Http, + Redirect(String), +} + #[derive(Deserialize)] struct Search { query: String, @@ -156,16 +162,45 @@ impl Server { )); match (self.http_port(), self.https_port()) { - (Some(http_port), None) => self.spawn(router, handle, http_port, None)?.await??, + (Some(http_port), None) => { + self + .spawn(router, handle, http_port, SpawnConfig::Http)? + .await?? + } (None, Some(https_port)) => { self - .spawn(router, handle, https_port, Some(self.acceptor(&options)?))? + .spawn( + router, + handle, + https_port, + SpawnConfig::Https(self.acceptor(&options)?), + )? .await?? } (Some(http_port), Some(https_port)) => { + let http_spawn_config = if self.redirect_http_to_https { + let acme_domain = Self::acme_domains(&self.acme_domain)? + .into_iter() + .nth(0) + .unwrap(); + + SpawnConfig::Redirect(if https_port == 443 { + format!("https://{acme_domain}") + } else { + format!("https://{acme_domain}:{https_port}") + }) + } else { + SpawnConfig::Http + }; + let (http_result, https_result) = tokio::join!( - self.spawn(router.clone(), handle.clone(), http_port, None)?, - self.spawn(router, handle, https_port, Some(self.acceptor(&options)?))? + self.spawn(router.clone(), handle.clone(), http_port, http_spawn_config)?, + self.spawn( + router, + handle, + https_port, + SpawnConfig::Https(self.acceptor(&options)?), + )? ); http_result.and(https_result)??; } @@ -181,7 +216,7 @@ impl Server { router: Router, handle: Handle, port: u16, - https_acceptor: Option, + config: SpawnConfig, ) -> Result>> { let addr = (self.address.as_str(), port) .to_socket_addrs()? @@ -191,47 +226,39 @@ impl Server { if !integration_test() { eprintln!( "Listening on {}://{addr}", - if https_acceptor.is_some() { - "https" - } else { - "http" + match config { + SpawnConfig::Https(_) => "https", + _ => "http", } ); } - let redirect_destination = if self.redirect_http_to_https && https_acceptor.is_none() { - let acme_domain = Self::acme_domains(&self.acme_domain)? - .into_iter() - .nth(0) - .unwrap(); - - Some(format!("https://{acme_domain}")) - } else { - None - }; - Ok(tokio::spawn(async move { - if let Some(acceptor) = https_acceptor { - axum_server::Server::bind(addr) - .handle(handle) - .acceptor(acceptor) - .serve(router.into_make_service()) - .await - } else if let Some(redirect_destination) = redirect_destination { - axum_server::Server::bind(addr) - .handle(handle) - .serve( - Router::new() - .fallback(Self::redirect_http_to_https) - .layer(Extension(redirect_destination)) - .into_make_service(), - ) - .await - } else { - axum_server::Server::bind(addr) - .handle(handle) - .serve(router.into_make_service()) - .await + match config { + SpawnConfig::Https(acceptor) => { + axum_server::Server::bind(addr) + .handle(handle) + .acceptor(acceptor) + .serve(router.into_make_service()) + .await + } + SpawnConfig::Redirect(destination) => { + axum_server::Server::bind(addr) + .handle(handle) + .serve( + Router::new() + .fallback(Self::redirect_http_to_https) + .layer(Extension(destination)) + .into_make_service(), + ) + .await + } + SpawnConfig::Http => { + axum_server::Server::bind(addr) + .handle(handle) + .serve(router.into_make_service()) + .await + } } })) } @@ -1114,7 +1141,7 @@ mod tests { #[test] fn http_to_https_redirect_with_path() { - TestServer::new_with_args(&[], &["--redirect-http-to-https"]).assert_redirect( + TestServer::new_with_args(&[], &["--redirect-http-to-https", "--https"]).assert_redirect( "/sat/0", &format!("https://{}/sat/0", sys_info::hostname().unwrap()), ); @@ -1122,7 +1149,7 @@ mod tests { #[test] fn http_to_https_redirect_with_empty() { - TestServer::new_with_args(&[], &["--redirect-http-to-https"]) + TestServer::new_with_args(&[], &["--redirect-http-to-https", "--https"]) .assert_redirect("/", &format!("https://{}/", sys_info::hostname().unwrap())); } From 740a13c6cf38636ce8396da19c4b54fe8425c014 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Wed, 11 Jan 2023 16:29:29 -0800 Subject: [PATCH 3/3] Fix tests --- src/subcommand/server.rs | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 39c725ad4d..af683e059c 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -179,15 +179,12 @@ impl Server { } (Some(http_port), Some(https_port)) => { let http_spawn_config = if self.redirect_http_to_https { - let acme_domain = Self::acme_domains(&self.acme_domain)? - .into_iter() - .nth(0) - .unwrap(); + let acme_domains = self.acme_domains()?; SpawnConfig::Redirect(if https_port == 443 { - format!("https://{acme_domain}") + format!("https://{}", acme_domains[0]) } else { - format!("https://{acme_domain}:{https_port}") + format!("https://{}:{https_port}", acme_domains[0]) }) } else { SpawnConfig::Http @@ -273,9 +270,9 @@ impl Server { Ok(acme_cache) } - fn acme_domains(acme_domain: &Vec) -> Result> { - if !acme_domain.is_empty() { - Ok(acme_domain.clone()) + fn acme_domains(&self) -> Result> { + if !self.acme_domain.is_empty() { + Ok(self.acme_domain.clone()) } else { Ok(vec![sys_info::hostname()?]) } @@ -298,7 +295,7 @@ impl Server { } fn acceptor(&self, options: &Options) -> Result { - let config = AcmeConfig::new(Self::acme_domains(&self.acme_domain)?) + let config = AcmeConfig::new(self.acme_domains()?) .contact(&self.acme_contact) .cache_option(Some(DirCache::new(Self::acme_cache( self.acme_cache.as_ref(), @@ -1063,18 +1060,17 @@ mod tests { #[test] fn acme_domain_defaults_to_hostname() { + let (_, server) = parse_server_args("ord server"); assert_eq!( - Server::acme_domains(&Vec::new()).unwrap(), + server.acme_domains().unwrap(), &[sys_info::hostname().unwrap()] ); } #[test] fn acme_domain_flag_is_respected() { - assert_eq!( - Server::acme_domains(&vec!["example.com".into()]).unwrap(), - &["example.com"] - ); + let (_, server) = parse_server_args("ord server --acme-domain example.com"); + assert_eq!(server.acme_domains().unwrap(), &["example.com"]); } #[test]