diff --git a/hexstody-btc-test/src/runner.rs b/hexstody-btc-test/src/runner.rs index 40e3c92..056e550 100644 --- a/hexstody-btc-test/src/runner.rs +++ b/hexstody-btc-test/src/runner.rs @@ -97,6 +97,8 @@ async fn setup_api(rpc_port: u16) -> u16 { let state = state.clone(); let polling_duration = Duration::from_secs(1); let client = make_client(); + // 64 0es encoded to base64 + let secret_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="; async move { serve_public_api( client, @@ -106,6 +108,7 @@ async fn setup_api(rpc_port: u16) -> u16 { state, state_notify, polling_duration, + secret_key, ) .await .expect("start api"); diff --git a/hexstody-btc/src/api/public.rs b/hexstody-btc/src/api/public.rs index 94afef9..7642b9d 100644 --- a/hexstody-btc/src/api/public.rs +++ b/hexstody-btc/src/api/public.rs @@ -66,12 +66,14 @@ pub async fn serve_public_api( state: Arc>, state_notify: Arc, polling_duration: Duration, + secret_key: &str, ) -> Result<(), rocket::Error> { let figment = Figment::from(Config { address, port, ..Config::default() }) + .merge(("secret_key", secret_key)) .merge(Env::prefixed("HEXSTODY_BTC_").global()); let on_ready = AdHoc::on_liftoff("API Start!", |_| { diff --git a/hexstody-btc/src/main.rs b/hexstody-btc/src/main.rs index 328e102..fb3d0aa 100644 --- a/hexstody-btc/src/main.rs +++ b/hexstody-btc/src/main.rs @@ -52,6 +52,12 @@ enum SubCommand { node_password: String, #[clap(long, default_value = "bitcoin", env = "HEXSTODY_BTC_NODE_NETWORK")] network: Network, + #[clap( + long, + env = "HEXSTODY_BTC_SECRET_KEY", + hide_env_values = true, + )] + secret_key: String, }, } @@ -76,6 +82,7 @@ async fn main() -> Result<(), Box> { node_user, node_password, network, + secret_key, } => loop { let (abort_handle, abort_reg) = AbortHandle::new_pair(); ctrlc::set_handler(move || { @@ -116,6 +123,7 @@ async fn main() -> Result<(), Box> { state.clone(), state_notify.clone(), polling_duration, + &secret_key, ) .await; res.map_err(|err| LogicError::from(err)) diff --git a/hexstody-hot/src/api/operator/mod.rs b/hexstody-hot/src/api/operator/mod.rs index 0d80634..9b4afe1 100644 --- a/hexstody-hot/src/api/operator/mod.rs +++ b/hexstody-hot/src/api/operator/mod.rs @@ -80,10 +80,14 @@ pub async fn serve_operator_api( _start_notify: Arc, port: u16, update_sender: mpsc::Sender, + secret_key: String, + static_path: String, ) -> Result<(), rocket::Error> { - let figment = rocket::Config::figment().merge(("port", port)); + let figment = rocket::Config::figment() + .merge(("secret_key", secret_key)) + .merge(("port", port)); rocket::custom(figment) - .mount("/", FileServer::from(relative!("static/"))) + .mount("/", FileServer::from(static_path)) .mount("/", routes![index]) .mount("/", openapi_get_routes![list, create]) .mount( diff --git a/hexstody-hot/src/api/public/mod.rs b/hexstody-hot/src/api/public/mod.rs index e488443..9a3f48a 100644 --- a/hexstody-hot/src/api/public/mod.rs +++ b/hexstody-hot/src/api/public/mod.rs @@ -13,8 +13,8 @@ use hexstody_db::Pool; use rocket::fairing::AdHoc; use rocket::fs::{relative, FileServer}; use rocket::response::{content, Redirect}; -use rocket::uri; use rocket::serde::json::Json; +use rocket::uri; use rocket::{get, routes}; use rocket_dyn_templates::Template; use rocket_okapi::{openapi, openapi_get_routes, swagger_ui::*}; @@ -96,8 +96,12 @@ pub async fn serve_public_api( port: u16, update_sender: mpsc::Sender, btc_client: BtcClient, + secret_key: String, + static_path: String, ) -> Result<(), rocket::Error> { - let figment = rocket::Config::figment().merge(("port", port)); + let figment = rocket::Config::figment() + .merge(("secret_key", secret_key)) + .merge(("port", port)); let on_ready = AdHoc::on_liftoff("API Start!", |_| { Box::pin(async move { start_notify.notify_one(); @@ -105,7 +109,7 @@ pub async fn serve_public_api( }); rocket::custom(figment) - .mount("/", FileServer::from(relative!("static/"))) + .mount("/", FileServer::from(static_path)) .mount( "/", openapi_get_routes![ @@ -166,6 +170,9 @@ mod tests { let state = state_mx.clone(); let state_notify = state_notify.clone(); let start_notify = start_notify.clone(); + // 64 0es encoded to base64 + let secret_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==".to_owned(); + let static_path = relative!("static/"); async move { let serve_task = serve_public_api( pool, @@ -175,6 +182,8 @@ mod tests { SERVICE_TEST_PORT, update_sender, btc_client, + secret_key, + static_path.to_owned(), ); futures::pin_mut!(serve_task); futures::future::select(serve_task, receiver.map_err(drop)).await; diff --git a/hexstody-hot/src/main.rs b/hexstody-hot/src/main.rs index c4bd0a9..aedf260 100644 --- a/hexstody-hot/src/main.rs +++ b/hexstody-hot/src/main.rs @@ -40,6 +40,18 @@ struct Args { network: Network, #[clap(long, env = "HEXSTODY_START_REGTEST")] start_regtest: bool, + #[clap( + long, + env = "HEXSTODY_SECRET_KEY", + hide_env_values = true, + )] + secret_key: String, + /// Path to HTML static files to serve + #[clap( + long, + env = "HEXSTODY_STATIC_PATH", + )] + static_path: Option, #[clap(subcommand)] subcmd: SubCommand, } @@ -84,6 +96,9 @@ async fn run(btc_client: BtcClient, args: &Args) { api_abort_handle.abort(); }) .expect("Error setting Ctrl-C handler"); + let relative = rocket::fs::relative!("static/").to_owned(); + let static_path = args.static_path.as_ref().unwrap_or(&relative); + match run_hot_wallet( args.network, api_config, @@ -91,6 +106,8 @@ async fn run(btc_client: BtcClient, args: &Args) { start_notify, btc_client.clone(), api_abort_reg, + &args.secret_key, + static_path, ) .await { diff --git a/hexstody-hot/src/runner.rs b/hexstody-hot/src/runner.rs index eeb553e..5670420 100644 --- a/hexstody-hot/src/runner.rs +++ b/hexstody-hot/src/runner.rs @@ -48,11 +48,11 @@ impl ApiConfig { pub fn parse_figment() -> Self { let figment = rocket::Config::figment(); let public_api_enabled = figment.extract_inner("public_api_enabled").unwrap_or(true); - let public_api_port = figment.extract_inner("public_api_port").unwrap_or(8000); + let public_api_port = figment.extract_inner("public_api_port").unwrap_or(9800); let operator_api_enabled = figment .extract_inner("operator_api_enabled") .unwrap_or(true); - let operator_api_port = figment.extract_inner("operator_api_port").unwrap_or(8001); + let operator_api_port = figment.extract_inner("operator_api_port").unwrap_or(9801); ApiConfig { public_api_enabled, public_api_port, @@ -93,6 +93,8 @@ async fn serve_api( abort_reg: AbortRegistration, update_sender: mpsc::Sender, btc_client: BtcClient, + secret_key: String, + static_path: String, ) -> () { if !api_enabled { info!("{api_type} API disabled"); @@ -110,6 +112,8 @@ async fn serve_api( port, update_sender.clone(), btc_client, + secret_key, + static_path, ) }) .await; @@ -123,6 +127,8 @@ async fn serve_api( start_notify.clone(), port, update_sender.clone(), + secret_key, + static_path, ) }) .await; @@ -139,6 +145,8 @@ pub async fn serve_apis( api_abort: AbortRegistration, update_sender: mpsc::Sender, btc_client: BtcClient, + secret_key: &str, + static_path: &str, ) -> Result<(), Aborted> { let (public_handle, public_abort) = AbortHandle::new_pair(); let public_api_fut = serve_api( @@ -152,6 +160,8 @@ pub async fn serve_apis( public_abort, update_sender.clone(), btc_client.clone(), + secret_key.to_owned(), + static_path.to_owned(), ); let (operator_handle, operator_abort) = AbortHandle::new_pair(); let operator_api_fut = serve_api( @@ -165,6 +175,8 @@ pub async fn serve_apis( operator_abort, update_sender.clone(), btc_client, + secret_key.to_owned(), + static_path.to_owned(), ); let abortable_apis = @@ -196,6 +208,8 @@ pub async fn run_hot_wallet( start_notify: Arc, btc_client: BtcClient, api_abort_reg: AbortRegistration, + secret_key: &str, + static_path: &str, ) -> Result<(), Error> { info!("Connecting to database"); let pool = create_db_pool(db_connect).await?; @@ -231,6 +245,8 @@ pub async fn run_hot_wallet( api_abort_reg, update_sender, btc_client, + secret_key, + static_path, ) .await { diff --git a/hexstody-hot/src/tests/runner.rs b/hexstody-hot/src/tests/runner.rs index 7f5244d..b0ae3df 100644 --- a/hexstody-hot/src/tests/runner.rs +++ b/hexstody-hot/src/tests/runner.rs @@ -44,7 +44,9 @@ where let mut api_config = ApiConfig::parse_figment(); api_config.public_api_port = public_api_port; api_config.operator_api_port = operator_api_port; - + // 64 0es encoded to base64 + let secret_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="; + let static_path = rocket::fs::relative!("static/"); let (_, abort_reg) = AbortHandle::new_pair(); match run_hot_wallet( Network::Regtest, @@ -53,6 +55,8 @@ where start_notify, btc_adapter, abort_reg, + secret_key, + static_path, ) .await { diff --git a/nix/hexstody-btc.nix b/nix/hexstody-btc.nix new file mode 100644 index 0000000..2170d3d --- /dev/null +++ b/nix/hexstody-btc.nix @@ -0,0 +1,119 @@ +{ config, pkgs, lib, ... }: +with lib; # use the functions from lib, such as mkIf +let + # the values of the options set for the service by the user of the service + cfg = config.services.hexstody-btc; +in { + ##### interface. here we define the options that users of our service can specify + options = { + # the options for our service will be located under services.hexstody-btc + services.hexstody-btc = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable hexstody BTC adapter service by default. + ''; + }; + package = mkOption { + type = types.package; + default = pkgs.hexstody; + description = '' + Which package to use with the service. + ''; + }; + port = mkOption { + type = types.int; + default = 8180; + description = '' + Which port the BTC adapter listen to serve API. + ''; + }; + host = mkOption { + type = types.str; + default = "0.0.0.0"; + description = '' + Which hostname is binded to the node. + ''; + }; + + btcNode = mkOption { + type = types.str; + default = "127.0.0.1:8332/wallet/hexstody"; + description = '' + Host and port where BTC RPC node is located. + ''; + }; + rpcUser = mkOption { + type = types.str; + default = "bitcoin"; + description = '' + Which name of bitcoin RPC user to use. + ''; + }; + passwordFile = mkOption { + type = types.str; + default = "/run/keys/hexstodybtcrpc"; + description = '' + Location of file with password for RPC. + ''; + }; + passwordFileService = mkOption { + type = types.str; + default = "hexstodybtcrpc-key.service"; + description = '' + Service that indicates that passwordFile is ready. + ''; + }; + secretKey = mkOption { + type = types.str; + default = "/run/keys/hexstodybtccookieskey"; + description = '' + Location of file with cookies secret key. + ''; + }; + secretKeyService = mkOption { + type = types.str; + default = "hexstodybtccookies-key.service"; + description = '' + Service that indicates that secretKey is ready. + ''; + }; + }; + }; + + ##### implementation + config = mkIf cfg.enable { # only apply the following settings if enabled + # User to run the node + users.users.hexstody-btc = { + name = "hexstody-btc"; + group = "hexstody-btc"; + description = "hexstody-btc daemon user"; + isSystemUser = true; + }; + users.groups.hexstody-btc = {}; + # Create systemd service + systemd.services.hexstody-btc = { + enable = true; + description = "Hexstody BTC adapter"; + after = ["network.target" cfg.passwordFileService cfg.secretKeyService]; + wants = ["network.target" cfg.passwordFileService cfg.secretKeyService]; + script = '' + export HEXSTODY_BTC_NODE_PASSWORD=$(cat ${cfg.passwordFile} | xargs echo -n) + export HEXSTODY_BTC_SECRET_KEY=$(cat ${cfg.secretKey} | xargs echo -n) + ${cfg.package}/bin/hexstody-btc serve \ + --address ${cfg.host} \ + --node-url ${cfg.btcNode} \ + --node-user ${cfg.rpcUser} \ + --port ${builtins.toString cfg.port} + ''; + serviceConfig = { + Restart = "always"; + RestartSec = 30; + User = "hexstody-btc"; + LimitNOFILE = 65536; + }; + wantedBy = ["multi-user.target"]; + }; + }; +} diff --git a/nix/hexstody-hot.nix b/nix/hexstody-hot.nix new file mode 100644 index 0000000..68f6cad --- /dev/null +++ b/nix/hexstody-hot.nix @@ -0,0 +1,135 @@ +{ config, pkgs, lib, ... }: +with lib; # use the functions from lib, such as mkIf +let + # the values of the options set for the service by the user of the service + cfg = config.services.hexstody-hot; +in { + ##### interface. here we define the options that users of our service can specify + options = { + # the options for our service will be located under services.hexstody-hot + services.hexstody-hot = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable hexstody hot wallet service by default. + ''; + }; + package = mkOption { + type = types.package; + default = pkgs.hexstody; + description = '' + Which package to use with the service. + ''; + }; + port = mkOption { + type = types.int; + default = 8180; + description = '' + Which port the BTC adapter listen to serve API. + ''; + }; + host = mkOption { + type = types.str; + default = "0.0.0.0"; + description = '' + Which hostname is binded to the node. + ''; + }; + + btcModule = mkOption { + type = types.str; + default = "http://127.0.0.1:8180"; + description = '' + Host and port where BTC adapter service is located. + ''; + }; + databaseHost = mkOption { + type = types.str; + default = "localhost:5432"; + description = '' + Connection host to the database. + ''; + }; + databaseName = mkOption { + type = types.str; + default = "hexstody"; + description = '' + Database name. + ''; + }; + databaseUser = mkOption { + type = types.str; + default = "hexstody"; + description = '' + User name for database. + ''; + }; + passwordFile = mkOption { + type = types.str; + default = "/run/keys/hexstodydb"; + description = '' + Location of file with password for database. + ''; + }; + passwordFileService = mkOption { + type = types.str; + default = "hexstodydb-key.service"; + description = '' + Service that indicates that passwordFile is ready. + ''; + }; + secretKey = mkOption { + type = types.str; + default = "/run/keys/hexstodycookieskey"; + description = '' + Location of file with cookies secret key. + ''; + }; + secretKeyService = mkOption { + type = types.str; + default = "hexstodycookies-key.service"; + description = '' + Service that indicates that secretKey is ready. + ''; + }; + }; + }; + + ##### implementation + config = mkIf cfg.enable { # only apply the following settings if enabled + # User to run the node + users.users.hexstody = { + name = "hexstody"; + group = "hexstody"; + extraGroups = [ "tor" ]; + description = "hexstody daemon user"; + isSystemUser = true; + }; + users.groups.hexstody = {}; + # Create systemd service + systemd.services.hexstody-hot = { + enable = true; + description = "Hexstody hot wallet"; + after = ["network.target" cfg.passwordFileService cfg.secretKeyService]; + wants = ["network.target" cfg.passwordFileService cfg.secretKeyService]; + script = '' + export DB_PASSWORD=$(cat ${cfg.passwordFile} | xargs echo -n) + export DATABASE_URL="postgresql://${cfg.databaseUser}:$DB_PASSWORD@${cfg.databaseHost}/${cfg.databaseName}" + export HEXSTODY_SECRET_KEY=$(cat ${cfg.secretKey} | xargs echo -n) + cd ${cfg.package}/share + ${cfg.package}/bin/hexstody-hot \ + --btc-module ${cfg.btcModule} \ + --static-path ${cfg.package}/share/static \ + serve + ''; + serviceConfig = { + Restart = "always"; + RestartSec = 30; + User = "hexstody"; + LimitNOFILE = 65536; + }; + wantedBy = ["multi-user.target"]; + }; + }; +} diff --git a/nix/hexstody.nix b/nix/hexstody.nix new file mode 100644 index 0000000..cfbb546 --- /dev/null +++ b/nix/hexstody.nix @@ -0,0 +1,76 @@ +{config, lib, pkgs, ...}: +{ + imports = [ + ./hexstody-btc.nix + ./hexstody-hot.nix + ]; + config = { + nixpkgs.overlays = [ + (import ./overlay.nix) + ]; + services.hexstody-btc = { + enable = true; + }; + services.bitcoin.extraConfig = '' + wallet=hexstody + ''; + services.hexstody-hot = { + enable = true; + }; + systemd.services = { + hexstodybtcrpc-key = { + enable = true; + description = "BTC key is provided"; + wantedBy = [ "network.target" ]; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + script = + '' + echo "BTC key is done" + ''; + }; + hexstodydb-key = { + enable = true; + description = "Database password is provided"; + wantedBy = [ "network.target" ]; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + script = + '' + echo "Database password is done" + ''; + }; + hexstodybtccookies-key = { + enable = true; + description = "Cookies key is provided"; + wantedBy = [ "network.target" ]; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + script = + '' + echo "Cookies key is done" + ''; + }; + hexstodycookies-key = { + enable = true; + description = "Cookies key is provided"; + wantedBy = [ "network.target" ]; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + script = + '' + echo "Cookies key is done" + ''; + }; + }; + services.postgresql = { + ensureDatabases = [ "hexstody" ]; + ensureUsers = [ + { + name = "hexstody"; + ensurePermissions."DATABASE hexstody" = "ALL PRIVILEGES"; + } + ]; + }; + }; +} \ No newline at end of file diff --git a/nix/overlay.nix b/nix/overlay.nix index 59956a1..99104c0 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -1,4 +1,4 @@ self: super: rec { - eclair-tortoise = import ../default.nix; + hexstody = import ../default.nix; }