Skip to content

Commit

Permalink
Allow serving HTTP and HTTPS simultaneously (#359)
Browse files Browse the repository at this point in the history
  • Loading branch information
casey committed Aug 31, 2022
1 parent 99dc2cd commit 335becc
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 50 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ rayon = "1.5.1"
redb = { version = "0.5.0", git = "https://github.com/cberner/redb", branch = "master" }
rust-embed = "6.4.0"
rustls = "0.20.6"
rustls-acme = { version = "0.4.0-beta2", features = ["axum"] }
rustls-acme = { version = "0.5.0", features = ["axum"] }
serde = { version = "1.0.137", features = ["derive"] }
serde_cbor = "0.11.2"
serde_json = "1.0.81"
Expand Down
3 changes: 2 additions & 1 deletion deploy/ord.service
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ ExecStart=/usr/local/bin/ord \
--chain ${CHAIN} \
server \
--acme-contact mailto:casey@rodarmor.com \
--https-port 443
--http \
--https
Group=ord
MemoryDenyWriteExecute=true
NoNewPrivileges=true
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ use {
thread,
time::{Duration, Instant},
},
tokio::runtime::Runtime,
tokio::{runtime::Runtime, task},
tower_http::cors::{Any, CorsLayer},
};

Expand Down
192 changes: 147 additions & 45 deletions src/subcommand/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ use {
},
},
axum::{body, http::header, response::Response},
clap::ArgGroup,
rust_embed::RustEmbed,
rustls_acme::{
acme::{LETS_ENCRYPT_PRODUCTION_DIRECTORY, LETS_ENCRYPT_STAGING_DIRECTORY},
Expand Down Expand Up @@ -47,7 +46,6 @@ impl Display for StaticHtml {
}

#[derive(Debug, Parser)]
#[clap(group = ArgGroup::new("port").multiple(false))]
pub(crate) struct Server {
#[clap(
long,
Expand All @@ -62,20 +60,23 @@ pub(crate) struct Server {
acme_domain: Vec<String>,
#[clap(
long,
group = "port",
help = "Listen on <HTTP_PORT> for incoming HTTP requests. Defaults to 80."
help = "Listen on <HTTP_PORT> for incoming HTTP requests. [default: 80]."
)]
http_port: Option<u16>,
#[clap(
long,
group = "port",
help = "Listen on <HTTPS_PORT> for incoming HTTPS requests."
help = "Listen on <HTTPS_PORT> for incoming HTTPS requests. [default: 443]."
)]
https_port: Option<u16>,
#[structopt(long, help = "Store ACME TLS certificates in <ACME_CACHE>.")]
#[clap(long, help = "Store ACME TLS certificates in <ACME_CACHE>.")]
acme_cache: Option<PathBuf>,
#[structopt(long, help = "Provide ACME contact <ACME_CONTACT>.")]
#[clap(long, help = "Provide ACME contact <ACME_CONTACT>.")]
acme_contact: Vec<String>,
#[clap(long, help = "Serve HTTP traffic on <HTTP_PORT>.")]
http: bool,
#[clap(long, help = "Serve HTTPS traffic on <HTTPS_PORT>.")]
https: bool,
}

impl Server {
Expand All @@ -91,7 +92,7 @@ impl Server {
thread::sleep(Duration::from_millis(100));
});

let app = Router::new()
let router = Router::new()
.route("/", get(Self::home))
.route("/block/:hash", get(Self::block))
.route("/bounties", get(Self::bounties))
Expand All @@ -113,35 +114,58 @@ impl Server {
.allow_origin(Any),
);

let port = self.port();

let addr = (self.address.as_str(), port)
.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow!("Failed to get socket addrs"))?;

let handle = Handle::new();

LISTENERS.lock().unwrap().push(handle.clone());

let server = axum_server::Server::bind(addr).handle(handle);

match self.acceptor(&options)? {
Some(acceptor) => {
server
.acceptor(acceptor)
.serve(app.into_make_service())
.await?
}
None => server.serve(app.into_make_service()).await?,
}
let (http_result, https_result) = tokio::join!(
self.spawn(&router, &handle, None)?,
self.spawn(&router, &handle, self.acceptor(&options)?)?
);
http_result.and(https_result)?.transpose()?;

Ok(())
})
}

fn port(&self) -> u16 {
self.http_port.or(self.https_port).unwrap_or(80)
fn spawn(
&self,
router: &Router,
handle: &Handle,
https_acceptor: Option<AxumAcceptor>,
) -> Result<task::JoinHandle<Option<io::Result<()>>>> {
let addr = if https_acceptor.is_some() {
self.https_port()
} else {
self.http_port()
}
.map(|port| {
(self.address.as_str(), port)
.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow!("Failed to get socket addrs"))
.map(|addr| (addr, router.clone(), handle.clone()))
})
.transpose()?;

Ok(tokio::spawn(async move {
if let Some((addr, router, handle)) = addr {
Some(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
})
} else {
None
}
}))
}

fn acme_cache(acme_cache: Option<&PathBuf>, options: &Options) -> Result<PathBuf> {
Expand All @@ -160,8 +184,24 @@ impl Server {
}
}

fn http_port(&self) -> Option<u16> {
if self.http || self.http_port.is_some() || (self.https_port.is_none() && !self.https) {
Some(self.http_port.unwrap_or(80))
} else {
None
}
}

fn https_port(&self) -> Option<u16> {
if self.https || self.https_port.is_some() {
Some(self.https_port.unwrap_or(443))
} else {
None
}
}

fn acceptor(&self, options: &Options) -> Result<Option<AxumAcceptor>> {
if self.https_port.is_some() {
if self.https_port().is_some() {
let config = AcmeConfig::new(Self::acme_domains(&self.acme_domain)?)
.contact(&self.acme_contact)
.cache_option(Some(DirCache::new(Self::acme_cache(
Expand Down Expand Up @@ -443,27 +483,89 @@ impl Server {
mod tests {
use super::*;

#[test]
fn port_defaults_to_80() {
match Arguments::try_parse_from(&["ord", "server"])
.unwrap()
.subcommand
{
Subcommand::Server(server) => assert_eq!(server.port(), 80),
subcommand => panic!("Unexpected subcommand: {subcommand:?}"),
fn parse_server_args(args: &str) -> Server {
match Arguments::try_parse_from(
["ord", "server"]
.iter()
.cloned()
.chain(args.split_whitespace()),
) {
Ok(arguments) => match arguments.subcommand {
Subcommand::Server(server) => server,
subcommand => panic!("Unexpected subcommand: {subcommand:?}"),
},
Err(err) => panic!("Error parsing arguments: {err}"),
}
}

#[test]
fn http_and_https_port_conflict() {
let err = Arguments::try_parse_from(&["ord", "server", "--http-port=0", "--https-port=0"])
.unwrap_err()
.to_string();
fn http_and_https_port_dont_conflict() {
parse_server_args(
"--http-port 0 --https-port 0 --acme-cache foo --acme-contact bar --acme-domain baz",
);
}

#[test]
fn http_port_defaults_to_80() {
assert_eq!(parse_server_args("").http_port(), Some(80));
}

#[test]
fn https_port_defaults_to_none() {
assert_eq!(parse_server_args("").https_port(), None);
}

#[test]
fn https_sets_https_port_to_443() {
assert_eq!(
parse_server_args("--https --acme-cache foo --acme-contact bar --acme-domain baz")
.https_port(),
Some(443)
);
}

#[test]
fn https_disables_http() {
assert_eq!(
parse_server_args("--https --acme-cache foo --acme-contact bar --acme-domain baz")
.http_port(),
None
);
}

#[test]
fn https_port_disables_http() {
assert_eq!(
parse_server_args("--https-port 433 --acme-cache foo --acme-contact bar --acme-domain baz")
.http_port(),
None
);
}

#[test]
fn https_port_sets_https_port() {
assert_eq!(
parse_server_args("--https-port 1000 --acme-cache foo --acme-contact bar --acme-domain baz")
.https_port(),
Some(1000)
);
}

#[test]
fn http_with_https_leaves_http_enabled() {
assert_eq!(
parse_server_args("--https --http --acme-cache foo --acme-contact bar --acme-domain baz")
.http_port(),
Some(80)
);
}

assert!(
err.starts_with("error: The argument '--http-port <HTTP_PORT>' cannot be used with '--https-port <HTTPS_PORT>'\n"),
"{}",
err
#[test]
fn http_with_https_leaves_https_enabled() {
assert_eq!(
parse_server_args("--https --http --acme-cache foo --acme-contact bar --acme-domain baz")
.https_port(),
Some(443)
);
}

Expand Down

0 comments on commit 335becc

Please sign in to comment.