Browse Source

Password protect the maker's routes

no-contract-setup-message
Thomas Eizinger 3 years ago
parent
commit
dda021b37c
No known key found for this signature in database GPG Key ID: 651AC83A6C6C8B96
  1. 12
      Cargo.lock
  2. 3
      Cargo.toml
  3. 4
      daemon/Cargo.toml
  4. 84
      daemon/src/auth.rs
  5. 8
      daemon/src/maker.rs
  6. 117
      daemon/src/routes_maker.rs
  7. 11
      daemon/src/seed.rs

12
Cargo.lock

@ -423,9 +423,11 @@ dependencies = [
"cfd_protocol",
"clap",
"futures",
"hex",
"hkdf",
"rand 0.6.5",
"rocket",
"rocket-basicauth",
"rocket_db_pools",
"rust-embed",
"rust_decimal",
@ -1659,6 +1661,16 @@ dependencies = [
"yansi",
]
[[package]]
name = "rocket-basicauth"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79128c0f55b7bc6785c13816d71af4baee156bd615b09468800edaaa7da56a08"
dependencies = [
"base64 0.13.0",
"rocket",
]
[[package]]
name = "rocket_codegen"
version = "0.5.0-rc.1"

3
Cargo.toml

@ -1,3 +1,6 @@
[workspace]
members = ["cfd_protocol", "daemon"]
resolver = "2"
[patch.crates-io]
rocket = { git = "https://github.com/SergioBenitez/Rocket" } # Need to patch rocket dependency of `rocket_basicauth` until there is an official release.

4
daemon/Cargo.toml

@ -9,9 +9,11 @@ bdk = { git = "https://github.com/bitcoindevkit/bdk/" }
cfd_protocol = { path = "../cfd_protocol" }
clap = "3.0.0-beta.4"
futures = { version = "0.3", default-features = false }
hex = "0.4"
hkdf = "0.11"
rand = "0.6"
rocket = { git = "https://github.com/SergioBenitez/Rocket", features = ["json"] }
rocket = { version = "0.5.0-rc.1", features = ["json"] }
rocket-basicauth = { version = "2", default-features = false }
rocket_db_pools = { git = "https://github.com/SergioBenitez/Rocket", features = ["sqlx_sqlite"] }
rust-embed = "6.2"
rust_decimal = { version = "1.16", features = ["serde-float", "serde-arbitrary-precision"] }

84
daemon/src/auth.rs

@ -0,0 +1,84 @@
use hex::FromHexError;
use rocket::http::Status;
use rocket::outcome::{try_outcome, IntoOutcome};
use rocket::request::{FromRequest, Outcome};
use rocket::{Request, State};
use rocket_basicauth::{BasicAuth, BasicAuthError};
use std::fmt;
use std::str::FromStr;
/// A request guard that can be included in handler definitions to enforce authentication.
pub struct Authenticated {}
#[derive(Debug)]
pub enum Error {
UnknownUser(String),
BadPassword,
InvalidEncoding(FromHexError),
BadBasicAuthHeader(BasicAuthError),
/// The auth password was not configured in Rocket's state.
MissingPassword,
NoAuthHeader,
}
#[derive(PartialEq)]
pub struct Password([u8; 32]);
impl From<[u8; 32]> for Password {
fn from(bytes: [u8; 32]) -> Self {
Self(bytes)
}
}
impl fmt::Display for Password {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", hex::encode(self.0))
}
}
impl FromStr for Password {
type Err = FromHexError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut bytes = [0u8; 32];
hex::decode_to_slice(s, &mut bytes)?;
Ok(Self(bytes))
}
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Authenticated {
type Error = Error;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let basic_auth = try_outcome!(req
.guard::<BasicAuth>()
.await
.map_failure(|(status, error)| (status, Error::BadBasicAuthHeader(error)))
.forward_then(|()| Outcome::Failure((Status::Unauthorized, Error::NoAuthHeader))));
let password = try_outcome!(req
.guard::<&'r State<Password>>()
.await
.map_failure(|(status, _)| (status, Error::MissingPassword)));
if basic_auth.username != "maker" {
return Outcome::Failure((
Status::Unauthorized,
Error::UnknownUser(basic_auth.username),
));
}
if &try_outcome!(basic_auth
.password
.parse::<Password>()
.map_err(Error::InvalidEncoding)
.into_outcome(Status::BadRequest))
!= password.inner()
{
return Outcome::Failure((Status::Unauthorized, Error::BadPassword));
}
Outcome::Success(Authenticated {})
}
}

8
daemon/src/maker.rs

@ -13,6 +13,7 @@ use std::path::PathBuf;
use std::time::Duration;
use tokio::sync::{mpsc, watch};
mod auth;
mod db;
mod keypair;
mod maker_cfd_actor;
@ -78,6 +79,10 @@ async fn main() -> Result<()> {
.await?;
let wallet_info = wallet.sync().unwrap();
let auth_password = seed.derive_auth_password::<auth::Password>();
println!("Auth password: {}", auth_password);
let oracle = schnorrsig::KeyPair::new(SECP256K1, &mut rand::thread_rng()); // TODO: Fetch oracle public key from oracle.
let (cfd_feed_sender, cfd_feed_receiver) = watch::channel::<Vec<Cfd>>(vec![]);
@ -97,6 +102,7 @@ async fn main() -> Result<()> {
.manage(cfd_feed_receiver)
.manage(order_feed_receiver)
.manage(wallet_feed_receiver)
.manage(auth_password)
.attach(Db::init())
.attach(AdHoc::try_on_ignite(
"SQL migrations",
@ -163,10 +169,12 @@ async fn main() -> Result<()> {
routes_maker::get_health_check
],
)
.register("/api", rocket::catchers![routes_maker::unauthorized])
.mount(
"/",
rocket::routes![routes_maker::dist, routes_maker::index],
)
.register("/", rocket::catchers![routes_maker::unauthorized])
.launch()
.await?;

117
daemon/src/routes_maker.rs

@ -1,10 +1,11 @@
use crate::auth::Authenticated;
use crate::maker_cfd_actor;
use crate::model::cfd::{Cfd, Order, Origin};
use crate::model::{Usd, WalletInfo};
use crate::routes::EmbeddedFileExt;
use crate::to_sse_event::ToSseEvent;
use anyhow::Result;
use rocket::http::{ContentType, Status};
use rocket::http::{ContentType, Header, Status};
use rocket::response::stream::EventStream;
use rocket::response::{status, Responder};
use rocket::serde::json::Json;
@ -21,6 +22,7 @@ pub async fn maker_feed(
rx_cfds: &State<watch::Receiver<Vec<Cfd>>>,
rx_order: &State<watch::Receiver<Option<Order>>>,
rx_wallet: &State<watch::Receiver<WalletInfo>>,
_auth: Authenticated,
) -> EventStream![] {
let mut rx_cfds = rx_cfds.inner().clone();
let mut rx_order = rx_order.inner().clone();
@ -70,6 +72,7 @@ pub struct CfdNewOrderRequest {
pub async fn post_sell_order(
order: Json<CfdNewOrderRequest>,
cfd_actor_inbox: &State<mpsc::UnboundedSender<maker_cfd_actor::Command>>,
_auth: Authenticated,
) -> Result<status::Accepted<()>, status::BadRequest<String>> {
let order = Order::from_default_with_price(order.price, Origin::Ours)
.map_err(|e| status::BadRequest(Some(e.to_string())))?
@ -83,6 +86,23 @@ pub async fn post_sell_order(
Ok(status::Accepted(None))
}
/// A "catcher" for all 401 responses, triggers the browser's basic auth implementation.
#[rocket::catch(401)]
pub fn unauthorized() -> PromptAuthentication {
PromptAuthentication {
inner: (),
www_authenticate: Header::new("WWW-Authenticate", r#"Basic charset="UTF-8"#),
}
}
/// A rocket responder that prompts the user to sign in to access the API.
#[derive(rocket::Responder)]
#[response(status = 401)]
pub struct PromptAuthentication {
inner: (),
www_authenticate: Header<'static>,
}
// // TODO: Shall we use a simpler struct for verification? AFAICT quantity is not
// // needed, no need to send the whole CFD either as the other fields can be generated from the
// order #[rocket::post("/order/confirm", data = "<cfd_confirm_order_request>")]
@ -113,13 +133,104 @@ pub fn get_health_check() {}
struct Asset;
#[rocket::get("/assets/<file..>")]
pub fn dist<'r>(file: PathBuf) -> impl Responder<'r, 'static> {
pub fn dist<'r>(file: PathBuf, _auth: Authenticated) -> impl Responder<'r, 'static> {
let filename = format!("assets/{}", file.display().to_string());
Asset::get(&filename).into_response(file)
}
#[rocket::get("/<_paths..>", format = "text/html")]
pub fn index<'r>(_paths: PathBuf) -> impl Responder<'r, 'static> {
pub fn index<'r>(_paths: PathBuf, _auth: Authenticated) -> impl Responder<'r, 'static> {
let asset = Asset::get("index.html").ok_or(Status::NotFound)?;
Ok::<(ContentType, Cow<[u8]>), Status>((ContentType::HTML, asset.data))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::Password;
use bdk::bitcoin::{Address, Amount, Network, PublicKey};
use rocket::http::{Header, Status};
use rocket::local::blocking::Client;
use rocket::{Build, Rocket};
use std::time::SystemTime;
use tokio::sync::mpsc;
#[test]
fn routes_are_password_protected() {
let client = Client::tracked(rocket()).unwrap();
let feed_response = client.get("/feed").dispatch();
let new_sell_order_response = client
.post("/order/sell")
.body(r#"{"price":"40000", "min_quantity":"100", "max_quantity":"10000"}"#)
.dispatch();
let index_response = client.get("/").header(ContentType::HTML).dispatch();
assert_eq!(feed_response.status(), Status::Unauthorized);
assert_eq!(new_sell_order_response.status(), Status::Unauthorized);
assert_eq!(index_response.status(), Status::Unauthorized);
}
#[test]
fn correct_password_grants_access() {
let client = Client::tracked(rocket()).unwrap();
let feed_response = client.get("/feed").header(auth_header()).dispatch();
let new_sell_order_response = client
.post("/order/sell")
.body(r#"{"price":"40000", "min_quantity":"100", "max_quantity":"10000"}"#)
.header(auth_header())
.dispatch();
let index_response = client
.get("/")
.header(ContentType::HTML)
.header(auth_header())
.dispatch();
assert_eq!(feed_response.status(), Status::Ok);
assert_eq!(new_sell_order_response.status(), Status::Accepted);
assert_eq!(index_response.status(), Status::NotFound); // we don't embed the files in the
// tests
}
/// Constructs a Rocket instance for testing.
fn rocket() -> Rocket<Build> {
let (_, state1) = watch::channel::<Vec<Cfd>>(vec![]);
let (_, state2) = watch::channel::<Option<Order>>(None);
let (_, state3) = watch::channel::<WalletInfo>(WalletInfo {
balance: Amount::ZERO,
address: Address::p2wpkh(
&PublicKey::new(
"0286cd889349ebc06b3165505b9c083df0a4147f554614ff207c10f16ff509578c"
.parse()
.unwrap(),
),
Network::Regtest,
)
.unwrap(),
last_updated_at: SystemTime::now(),
});
let (state4, actor) = mpsc::unbounded_channel::<maker_cfd_actor::Command>();
std::mem::forget(actor); // pretend the actor is running so we can don't panic in the route handler
rocket::build()
.manage(state1)
.manage(state2)
.manage(state3)
.manage(state4)
.manage(Password::from(*b"Now I'm feelin' so fly like a G6"))
.mount("/", rocket::routes![maker_feed, post_sell_order, index])
}
/// Creates an "Authorization" header that matches the password above,
/// in particular it has been created through:
/// ```
/// base64(maker:hex("Now I'm feelin' so fly like a G6"))
/// ```
fn auth_header() -> Header<'static> {
Header::new(
"Authorization",
"Basic bWFrZXI6NGU2Zjc3MjA0OTI3NmQyMDY2NjU2NTZjNjk2ZTI3MjA3MzZmMjA2NjZjNzkyMDZjNjk2YjY1MjA2MTIwNDczNg==",
)
}
}

11
daemon/src/seed.rs

@ -62,6 +62,17 @@ impl Seed {
Ok(ext_priv_key)
}
#[allow(dead_code)] // Not used by all binaries.
pub fn derive_auth_password<P: From<[u8; 32]>>(&self) -> P {
let mut password = [0u8; 32];
Hkdf::<Sha256>::new(None, &self.0)
.expand(b"HTTP_AUTH_PASSWORD", &mut password)
.expect("okm array is of correct length");
P::from(password)
}
}
impl Default for Seed {

Loading…
Cancel
Save