Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redirect HTTP to HTTPS #1198

Merged
merged 5 commits into from
Jan 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion deploy/ord.service
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ ExecStart=/usr/local/bin/ord \
server \
--acme-contact mailto:[email protected] \
--http \
--https
--https \
--redirect-http-to-https
Group=ord
MemoryDenyWriteExecute=true
NoNewPrivileges=true
Expand Down
174 changes: 127 additions & 47 deletions src/subcommand/server.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -52,6 +51,12 @@ impl FromStr for BlockQuery {
}
}

enum SpawnConfig {
Https(AxumAcceptor),
Http,
Redirect(String),
}

#[derive(Deserialize)]
struct Search {
query: String,
Expand Down Expand Up @@ -110,6 +115,8 @@ pub(crate) struct Server {
http: bool,
#[clap(long, help = "Serve HTTPS traffic on <HTTPS_PORT>.")]
https: bool,
#[clap(long, help = "Redirect HTTP traffic to HTTPS.")]
redirect_http_to_https: bool,
}

impl Server {
Expand Down Expand Up @@ -155,16 +162,42 @@ 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_domains = self.acme_domains()?;

SpawnConfig::Redirect(if https_port == 443 {
format!("https://{}", acme_domains[0])
} else {
format!("https://{}:{https_port}", acme_domains[0])
})
} 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)??;
}
Expand All @@ -180,7 +213,7 @@ impl Server {
router: Router,
handle: Handle,
port: u16,
https_acceptor: Option<AxumAcceptor>,
config: SpawnConfig,
) -> Result<task::JoinHandle<io::Result<()>>> {
let addr = (self.address.as_str(), port)
.to_socket_addrs()?
Expand All @@ -190,26 +223,39 @@ impl Server {
if !integration_test() {
eprintln!(
"Listening on {}://{addr}",
if https_acceptor.is_some() {
"https"
} else {
"http"
match config {
SpawnConfig::Https(_) => "https",
_ => "http",
}
);
}

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 {
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
}
}
}))
}
Expand All @@ -224,9 +270,9 @@ impl Server {
Ok(acme_cache)
}

fn acme_domains(acme_domain: &Vec<String>) -> Result<Vec<String>> {
if !acme_domain.is_empty() {
Ok(acme_domain.clone())
fn acme_domains(&self) -> Result<Vec<String>> {
if !self.acme_domain.is_empty() {
Ok(self.acme_domain.clone())
} else {
Ok(vec![sys_info::hostname()?])
}
Expand All @@ -249,7 +295,7 @@ impl Server {
}

fn acceptor(&self, options: &Options) -> Result<AxumAcceptor> {
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(),
Expand Down Expand Up @@ -678,6 +724,17 @@ impl Server {
.page(chain, index.has_sat_index()?),
)
}

async fn redirect_http_to_https(
Extension(mut destination): Extension<String>,
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)]
Expand All @@ -695,10 +752,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();
Expand All @@ -716,12 +777,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());
Expand All @@ -737,8 +799,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 {
Expand Down Expand Up @@ -992,18 +1059,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]
Expand Down Expand Up @@ -1068,6 +1134,20 @@ mod tests {
);
}

#[test]
fn http_to_https_redirect_with_path() {
TestServer::new_with_args(&[], &["--redirect-http-to-https", "--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", "--https"])
.assert_redirect("/", &format!("https://{}/", sys_info::hostname().unwrap()));
}

#[test]
fn status() {
TestServer::new().assert_response("/status", StatusCode::OK, "OK");
Expand Down Expand Up @@ -1187,7 +1267,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,
".*<title>Output 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0</title>.*<h1>Output <span class=monospace>4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0</span></h1>
Expand Down Expand Up @@ -1400,7 +1480,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
Expand All @@ -1411,7 +1491,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",
Expand All @@ -1420,7 +1500,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,
".*
Expand All @@ -1432,7 +1512,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<.*",
Expand Down Expand Up @@ -1504,7 +1584,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
Expand Down Expand Up @@ -1536,7 +1616,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),
Expand All @@ -1560,7 +1640,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),
Expand All @@ -1584,7 +1664,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),
Expand Down Expand Up @@ -1748,7 +1828,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 {
Expand Down