From 2350bc40e419e2617d38537b1bfbcd0a4934d2dc Mon Sep 17 00:00:00 2001 From: dkijania Date: Wed, 13 Jul 2022 11:27:09 +0200 Subject: [PATCH] feature: NPG-2188 added mock farm to vitup.It should allow to start multiple mocks at one machine + give user some control over starting and stopping particular mock --- Cargo.lock | 1 + .../vitup/mock-farm/postman_collection.json | 110 ++++++++ doc/api/vitup/mock-farm/v0.yaml | 113 ++++++++ doc/vitup/mock_farm.md | 27 ++ output.text | 0 vitup/.gitignore | 1 + vitup/Cargo.toml | 1 + vitup/example/mock-farm/config.yaml | 13 + vitup/src/cli/mod.rs | 9 +- vitup/src/cli/start/mock.rs | 31 ++- vitup/src/cli/start/mod.rs | 2 +- vitup/src/config/certs.rs | 6 +- vitup/src/mode/mock/config.rs | 7 + vitup/src/mode/mock/farm/config/mod.rs | 41 +++ vitup/src/mode/mock/farm/context.rs | 119 +++++++++ vitup/src/mode/mock/farm/controller.rs | 160 +++++++++++ vitup/src/mode/mock/farm/mod.rs | 9 + vitup/src/mode/mock/farm/rest/mod.rs | 252 ++++++++++++++++++ vitup/src/mode/mock/mod.rs | 1 + vitup/src/mode/mock/rest/mod.rs | 2 +- vitup/vole.trace | 2 - 21 files changed, 896 insertions(+), 11 deletions(-) create mode 100644 doc/api/vitup/mock-farm/postman_collection.json create mode 100644 doc/api/vitup/mock-farm/v0.yaml create mode 100644 doc/vitup/mock_farm.md create mode 100644 output.text create mode 100644 vitup/example/mock-farm/config.yaml create mode 100644 vitup/src/mode/mock/farm/config/mod.rs create mode 100644 vitup/src/mode/mock/farm/context.rs create mode 100644 vitup/src/mode/mock/farm/controller.rs create mode 100644 vitup/src/mode/mock/farm/mod.rs create mode 100644 vitup/src/mode/mock/farm/rest/mod.rs delete mode 100644 vitup/vole.trace diff --git a/Cargo.lock b/Cargo.lock index ffadc777..ef7194a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6805,6 +6805,7 @@ dependencies = [ "jortestkit", "json", "lazy_static", + "netstat2", "poldercast 0.14.0-dev", "proptest", "quickcheck", diff --git a/doc/api/vitup/mock-farm/postman_collection.json b/doc/api/vitup/mock-farm/postman_collection.json new file mode 100644 index 00000000..eeab3521 --- /dev/null +++ b/doc/api/vitup/mock-farm/postman_collection.json @@ -0,0 +1,110 @@ +{ + "info": { + "_postman_id": "a54a9a24-ef93-48b5-a447-09a5d61d5075", + "name": "Mock Farm", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "11181958" + }, + "item": [ + { + "name": "List all active environments", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://127.0.0.1:7070/api/v0/active", + "protocol": "https", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "7070", + "path": [ + "api", + "v0", + "active" + ] + } + }, + "response": [] + }, + { + "name": "Start new environment with name 'marek' and port = 10010", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "https://127.0.0.1:7070/api/v0/start/darek", + "protocol": "https", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "7070", + "path": [ + "api", + "v0", + "start", + "darek" + ] + } + }, + "response": [] + }, + { + "name": "Shutdown existing environment with name 'marek'", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "https://127.0.0.1:7070/api/v0/start/marek/10010", + "protocol": "https", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "7070", + "path": [ + "api", + "v0", + "start", + "marek", + "10010" + ] + } + }, + "response": [] + }, + { + "name": "Shutdown existing environment with name 'marek'", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "https://127.0.0.1:7070/api/v0/shutdown/marek", + "protocol": "https", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "7070", + "path": [ + "api", + "v0", + "shutdown", + "marek" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/doc/api/vitup/mock-farm/v0.yaml b/doc/api/vitup/mock-farm/v0.yaml new file mode 100644 index 00000000..07348e5d --- /dev/null +++ b/doc/api/vitup/mock-farm/v0.yaml @@ -0,0 +1,113 @@ +openapi: 3.0.2 + +info: + title: Mock farm REST API + description: Mock Farm Rest API v0 + version: 0.0.1 + contact: + url: 'https://github.com/input-output-hk/vit-testing/' + +servers: + - url: 'https://localhost' + +tags: + - name: active + - name: start + - name: shutdown + +paths: + '/api/v0/active': + get: + description: Lists active mock environments + operationId: Active + tags: + - active + responses: + '200': + description: Success + content: + application/json: + schema: + description: assigned port number + type: string + format: text + '400': + description: Mock env with given ID or port already exists + + '/api/v0/start/{env_name}': + post: + description: Starts new mock env with random free port + operationId: StartEnvRandomPort + tags: + - start + parameters: + - name: env_name + in: path + required: true + schema: + description: Environment name + type: string + pattern: '[0-9a-f]+' + responses: + '200': + description: Success + content: + application/json: + schema: + description: assigned port number + type: string + format: text + '400': + description: Mock env with given ID or port already exists + + '/api/v0/start/{env_name}/{port}': + post: + description: Starts new mock env with random free port + operationId: StartEnv + tags: + - start + parameters: + - name: env_name + in: path + required: true + schema: + description: Environment name + type: string + pattern: '[0-9a-f]+' + responses: + '200': + description: Success + content: + application/json: + schema: + description: assigned port number + type: string + format: text + '400': + description: Mock env with given ID or port already exists + + '/api/v0/shutdown/{env_name}': + post: + description: Shutdown new mock env with random free port + operationId: ShutdownEnv + tags: + - shutdown + parameters: + - name: env_name + in: path + required: true + schema: + description: Environment name + type: string + pattern: '[0-9a-f]+' + responses: + '200': + description: Success + content: + application/json: + schema: + description: assigned port number + type: string + format: text + '404': + description: Mock env with given ID was not found diff --git a/doc/vitup/mock_farm.md b/doc/vitup/mock_farm.md new file mode 100644 index 00000000..f0647978 --- /dev/null +++ b/doc/vitup/mock_farm.md @@ -0,0 +1,27 @@ + +## Mock Farm + +Mock farm is a simple extension for mock service. It allows to run more that one mock at once and give more control to user in term of starting and stopping particular mock instance. + +### Configuration + +This section describe configuration file which can be passed as argument for snapshot service: + +- `port`: port on which registration-service will be exposed, +- `working_directory`: path to folder which config files will be dumped, +- `mocks-port-range`: range of ports assigned for usage, +- `protocol`: decide whether mock farm should be exposed as http or https, +- `local`: should service be exposed on all network interfaces or only 127.0.0.1, +- `token`: token limiting access to environment. Must be provided in header `API-Token` for each request + +Note: it is recommended to run command from `vit-testing/vitup` folder (then no explicit paths are required to be provided). +Configuration file example is available under `vit-testing/vitup/example/mock-farm/config.yaml` + +### Start + +`vitup start mock-farm --config example\mock\mock-farm\config.yaml` + +### Documentation + +- [OpenApi](../api/vitup/mock-farm/v0.yaml) +- [Requests collection](../api/vitup/mock-farm/postman_collection.json) \ No newline at end of file diff --git a/output.text b/output.text new file mode 100644 index 00000000..e69de29b diff --git a/vitup/.gitignore b/vitup/.gitignore index 3a957656..b833fe05 100644 --- a/vitup/.gitignore +++ b/vitup/.gitignore @@ -9,3 +9,4 @@ /prod/ /validate/ /vole.trace +/mock_farm/ \ No newline at end of file diff --git a/vitup/Cargo.toml b/vitup/Cargo.toml index ad91762d..8a7ae1c0 100644 --- a/vitup/Cargo.toml +++ b/vitup/Cargo.toml @@ -34,6 +34,7 @@ voting-hir = { git = "https://github.com/input-output-hk/catalyst-toolbox.git", valgrind = { path = "../valgrind" } poldercast = { git = "https://github.com/primetype/poldercast.git", rev = "8305f1560392a9d26673ca996e7646c8834533ef" } rand = "0.8" +netstat2 = "0.9" time = { version = "0.3.7", features=["serde","serde-well-known","parsing"]} fake = { version = "2.2", features=['chrono','http']} strum = "0.21.0" diff --git a/vitup/example/mock-farm/config.yaml b/vitup/example/mock-farm/config.yaml new file mode 100644 index 00000000..877fa1c8 --- /dev/null +++ b/vitup/example/mock-farm/config.yaml @@ -0,0 +1,13 @@ +{ + "port": 7070, + "working_directory": "./mock_farm", + "mocks-port-range": { + "start": 8080, + "end": 9090 + }, + "protocol": { + "key_path": "./resources/tls/server.key", + "cert_path": "./resources/tls/server.crt" + }, + "local": true +} \ No newline at end of file diff --git a/vitup/src/cli/mod.rs b/vitup/src/cli/mod.rs index 7b64c7b3..6bde9c4d 100644 --- a/vitup/src/cli/mod.rs +++ b/vitup/src/cli/mod.rs @@ -8,7 +8,7 @@ pub mod validate; use self::time::TimeCommand; use crate::cli::generate::{CommitteeIdCommandArgs, QrCommandArgs, SnapshotCommandArgs}; use crate::cli::start::AdvancedStartCommandArgs; -use crate::cli::start::MockStartCommandArgs; +use crate::cli::start::{MockFarmCommand, MockStartCommandArgs}; use crate::Result; use diff::DiffCommand; use generate::DataCommandArgs; @@ -53,8 +53,10 @@ pub enum StartCommand { Quick(QuickStartCommandArgs), /// start advanced backend from scratch Advanced(AdvancedStartCommandArgs), - // start mock env + /// start mock env Mock(MockStartCommandArgs), + /// start multiple mock environments + MockFarm(MockFarmCommand), } impl StartCommand { @@ -63,6 +65,9 @@ impl StartCommand { Self::Quick(quick_start_command) => quick_start_command.exec(), Self::Advanced(advanced_start_command) => advanced_start_command.exec(), Self::Mock(mock_start_command) => mock_start_command.exec().map_err(Into::into), + Self::MockFarm(mock_farm_start_command) => { + mock_farm_start_command.exec().map_err(Into::into) + } } } } diff --git a/vitup/src/cli/start/mock.rs b/vitup/src/cli/start/mock.rs index 74902f62..aae2fa1e 100644 --- a/vitup/src/cli/start/mock.rs +++ b/vitup/src/cli/start/mock.rs @@ -1,4 +1,4 @@ -use crate::mode::mock::{read_config, start_rest_server, Configuration, Context}; +use crate::mode::mock::{farm, read_config, start_rest_server, Configuration, Context}; use std::sync::Mutex; use std::{path::PathBuf, sync::Arc}; use structopt::StructOpt; @@ -38,6 +38,31 @@ impl MockStartCommandArgs { } } +#[derive(StructOpt, Debug)] +#[structopt(setting = structopt::clap::AppSettings::ColoredHelp)] +pub struct MockFarmCommand { + /// path to config file + #[structopt(long = "config", short = "c")] + pub config: PathBuf, +} + +impl MockFarmCommand { + #[tokio::main] + pub async fn exec(self) -> Result<(), Error> { + let control_context = Arc::new(Mutex::new(farm::Context::new( + farm::read_config(&self.config).unwrap(), + ))); + tokio::spawn(async move { + farm::start_rest_server(control_context.clone()) + .await + .unwrap() + }) + .await + .map(|_| ()) + .map_err(Into::into) + } +} + #[derive(Debug, Error)] #[allow(clippy::large_enum_variant)] pub enum Error { @@ -52,5 +77,7 @@ pub enum Error { #[error(transparent)] Mock(#[from] crate::mode::mock::ContextError), #[error(transparent)] - ServerError(crate::mode::mock::RestError), + Farm(#[from] crate::mode::mock::farm::ContextError), + #[error(transparent)] + ServerError(#[from] crate::mode::mock::RestError), } diff --git a/vitup/src/cli/start/mod.rs b/vitup/src/cli/start/mod.rs index dc111b82..c158ad9f 100644 --- a/vitup/src/cli/start/mod.rs +++ b/vitup/src/cli/start/mod.rs @@ -3,5 +3,5 @@ mod mock; mod quick; pub use advanced::AdvancedStartCommandArgs; -pub use mock::{Error as MockError, MockStartCommandArgs}; +pub use mock::{Error as MockError, MockFarmCommand, MockStartCommandArgs}; pub use quick::QuickStartCommandArgs; diff --git a/vitup/src/config/certs.rs b/vitup/src/config/certs.rs index 996068fa..2c1bd3a4 100644 --- a/vitup/src/config/certs.rs +++ b/vitup/src/config/certs.rs @@ -23,9 +23,9 @@ impl Default for CertificatesBuilder { impl CertificatesBuilder { pub fn build>(self, working_dir: P) -> Result { - let working_dir = working_dir.as_ref().to_path_buf(); - - std::fs::create_dir_all(working_dir.join("tls"))?; + let mut working_dir = working_dir.as_ref().to_path_buf(); + working_dir = working_dir.join("tls"); + std::fs::create_dir_all(&working_dir)?; let key_path = working_dir.join(SERVER_KEY); let mut key_file = diff --git a/vitup/src/mode/mock/config.rs b/vitup/src/mode/mock/config.rs index c2e309ac..a898fec0 100644 --- a/vitup/src/mode/mock/config.rs +++ b/vitup/src/mode/mock/config.rs @@ -21,6 +21,13 @@ pub fn read_config>(config: P) -> Result { serde_json::from_str(&contents).map_err(Into::into) } +pub fn write_config>(configuration: &Configuration, path: P) -> Result<(), Error> { + let content = serde_json::to_string(&configuration)?; + use std::io::Write; + let mut file = std::fs::File::create(&path)?; + file.write_all(content.as_bytes()).map_err(Into::into) +} + #[derive(Debug, Error)] pub enum Error { #[error("cannot parse configuration")] diff --git a/vitup/src/mode/mock/farm/config/mod.rs b/vitup/src/mode/mock/farm/config/mod.rs new file mode 100644 index 00000000..42a2ec66 --- /dev/null +++ b/vitup/src/mode/mock/farm/config/mod.rs @@ -0,0 +1,41 @@ +use crate::Result; +use assert_fs::TempDir; +use core::ops::Range; +use serde::{Deserialize, Serialize}; +use std::path::Path; +use std::path::PathBuf; +use valgrind::Protocol; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Config { + pub port: u16, + #[serde(alias = "mocks-port-range")] + pub mocks_port_range: Range, + pub token: Option, + pub working_directory: PathBuf, + pub protocol: Protocol, + pub local: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + port: 8080, + mocks_port_range: 8080..9090, + token: None, + working_directory: TempDir::new().unwrap().into_persistent().to_path_buf(), + protocol: Default::default(), + local: true, + } + } +} + +pub fn read_config>(config: P) -> Result { + let config = config.as_ref(); + if !config.exists() { + return Err(crate::error::Error::CannotFindConfig(config.to_path_buf())); + } + + let contents = std::fs::read_to_string(&config)?; + serde_json::from_str(&contents).map_err(Into::into) +} diff --git a/vitup/src/mode/mock/farm/context.rs b/vitup/src/mode/mock/farm/context.rs new file mode 100644 index 00000000..15890c57 --- /dev/null +++ b/vitup/src/mode/mock/farm/context.rs @@ -0,0 +1,119 @@ +pub type ContextLock = Arc>; +use super::config::Config; +use super::MockBootstrap; +use super::MockController; +use crate::mode::mock::Logger; +use std::collections::HashMap; +use std::net::SocketAddr; +use std::path::PathBuf; +use std::sync::Arc; +use std::sync::Mutex; +use thiserror::Error; +use valgrind::Protocol; + +pub type MockId = String; +pub type MockState = HashMap; + +pub struct Context { + config: Config, + state: MockState, + address: SocketAddr, + logger: Logger, +} + +impl Context { + pub fn new(config: Config) -> Self { + Self { + address: if config.local { + ([127, 0, 0, 1], config.port).into() + } else { + ([0, 0, 0, 0], config.port).into() + }, + state: HashMap::new(), + config, + logger: Logger::new(), + } + } + + pub fn log>(&mut self, message: S) { + self.logger.log(message) + } + + pub fn logs(&self) -> Vec { + self.logger.logs() + } + + pub fn clear_logs(&mut self) { + self.logger.clear() + } + + pub fn protocol(&self) -> Protocol { + self.config.protocol.clone() + } + + pub fn address(&self) -> SocketAddr { + self.address + } + + pub fn working_dir(&self) -> PathBuf { + self.config.working_directory.clone() + } + + pub fn state_mut(&mut self) -> &mut MockState { + &mut self.state + } + + pub fn api_token(&self) -> Option { + self.config.token.clone() + } + + #[allow(dead_code)] + pub fn set_api_token(&mut self, api_token: String) { + self.config.token = Some(api_token); + } + + pub fn get_active_mocks(&self) -> HashMap { + self.state + .iter() + .map(|(id, controller)| (id.clone(), controller.port())) + .collect() + } + + pub fn shutdown_mock(&mut self, id: MockId) -> Result { + let mut controller = self.state.remove(&id).ok_or(Error::CannotFindMock(id))?; + let port = controller.port(); + controller.shutdown(); + Ok(port) + } + + pub fn start_mock_on_random_port(&mut self, id: MockId) -> Result { + let mock_controller = MockBootstrap::new(id.clone()) + .https() + .working_directory(self.config.working_directory.clone()) + .spawn()?; + let port = mock_controller.port(); + self.state.insert(id, mock_controller); + Ok(port) + } + + pub fn start_mock(&mut self, id: MockId, port: u16) -> Result { + let mock_controller = MockBootstrap::new(id.clone()) + .port(port) + .https() + .working_directory(self.config.working_directory.clone()) + .spawn()?; + let port = mock_controller.port(); + self.state.insert(id, mock_controller); + Ok(port) + } +} + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Controller(#[from] super::ControllerError), + #[error("cannot find mock env with id: {0}")] + CannotFindMock(MockId), +} diff --git a/vitup/src/mode/mock/farm/controller.rs b/vitup/src/mode/mock/farm/controller.rs new file mode 100644 index 00000000..62d4af94 --- /dev/null +++ b/vitup/src/mode/mock/farm/controller.rs @@ -0,0 +1,160 @@ +use crate::client::rest::VitupDisruptionRestClient; +use crate::client::rest::VitupRest; +use crate::config::CertificatesBuilder; +use crate::mode::mock::config::write_config; +use crate::mode::mock::farm::context::MockId; +use crate::mode::mock::Configuration; +use lazy_static::lazy_static; +use netstat2::{get_sockets_info, AddressFamilyFlags, ProtocolFlags}; +use reqwest::Url; +use serde::Serialize; +use std::path::Path; +use std::path::PathBuf; +use std::process::Child; +use std::process::Command; +use std::{ + collections::HashSet, + sync::atomic::{AtomicU16, Ordering}, +}; +use thiserror::Error; + +lazy_static! { + static ref NEXT_AVAILABLE_PORT_NUMBER: AtomicU16 = AtomicU16::new(10000); + static ref OCCUPIED_PORTS: HashSet = { + let af_flags = AddressFamilyFlags::IPV4; + let proto_flags = ProtocolFlags::TCP | ProtocolFlags::UDP; + get_sockets_info(af_flags, proto_flags) + .unwrap() + .into_iter() + .map(|s| s.local_port()) + .collect::>() + }; +} + +fn get_available_port() -> u16 { + loop { + let candidate_port = NEXT_AVAILABLE_PORT_NUMBER.fetch_add(1, Ordering::SeqCst); + if !(*OCCUPIED_PORTS).contains(&candidate_port) { + return candidate_port; + } + } +} + +pub struct MockBootstrap { + mock_id: MockId, + configuration: Configuration, + working_directory: PathBuf, + https: bool, +} + +impl MockBootstrap { + pub fn new(mock_id: MockId) -> Self { + Self { + mock_id, + configuration: Configuration { + port: get_available_port(), + token: None, + ideascale: false, + working_dir: PathBuf::new(), + protocol: valgrind::Protocol::Http, + local: false, + }, + https: true, + working_directory: PathBuf::new(), + } + } + + pub fn port(mut self, port: u16) -> Self { + self.configuration.port = port; + self + } + + pub fn working_directory>(mut self, working_directory: P) -> Self { + self.working_directory = working_directory.as_ref().to_path_buf(); + self + } + + pub fn https(mut self) -> Self { + self.https = true; + self + } + + pub fn spawn(mut self) -> Result { + if self.https { + let certs = CertificatesBuilder::default().build(&self.working_directory)?; + self.configuration.protocol = certs.into(); + } + + let mut config_path = self.working_directory.join(&self.mock_id); + std::fs::create_dir_all(&config_path)?; + config_path = config_path.join("config.yaml"); + write_config(&self.configuration, &config_path)?; + + let mut command = Command::new("vitup"); + command + .arg("start") + .arg("mock") + .arg("--config") + .arg(config_path); + + Ok(MockController { + mock_id: self.mock_id, + configuration: self.configuration, + process: command.spawn()?, + }) + } +} + +#[derive(Debug, Serialize)] +pub struct MockController { + mock_id: MockId, + configuration: Configuration, + #[serde(skip_serializing)] + process: Child, +} + +impl MockController { + pub fn port(&self) -> u16 { + self.configuration.port + } + + pub fn is_up(&self) -> bool { + let rest_client = { + if let Some(token) = &self.configuration.token { + VitupRest::new_with_token(token.clone(), self.address()) + } else { + VitupRest::new(self.address()) + } + }; + VitupDisruptionRestClient::from(rest_client).is_up() + } + + pub fn address(&self) -> String { + let mut url = Url::parse("http://127.0.0.1").unwrap(); + url.set_scheme(&self.configuration.protocol.schema()) + .unwrap(); + url.set_port(Some(self.configuration.port)).unwrap(); + url.to_string() + } + + pub fn shutdown(&mut self) { + let _ = self.process.kill(); + } +} + +impl Drop for MockController { + fn drop(&mut self) { + self.shutdown(); + self.process.wait().unwrap(); + } +} + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Certs(#[from] crate::config::certs::Error), + #[error(transparent)] + Config(#[from] crate::mode::mock::config::Error), +} diff --git a/vitup/src/mode/mock/farm/mod.rs b/vitup/src/mode/mock/farm/mod.rs new file mode 100644 index 00000000..4c9e872a --- /dev/null +++ b/vitup/src/mode/mock/farm/mod.rs @@ -0,0 +1,9 @@ +mod config; +mod context; +mod controller; +mod rest; + +pub use config::{read_config, Config}; +pub use context::{Context, ContextLock, Error as ContextError}; +pub use controller::{Error as ControllerError, MockBootstrap, MockController}; +pub use rest::start_rest_server; diff --git a/vitup/src/mode/mock/farm/rest/mod.rs b/vitup/src/mode/mock/farm/rest/mod.rs new file mode 100644 index 00000000..9e3e810f --- /dev/null +++ b/vitup/src/mode/mock/farm/rest/mod.rs @@ -0,0 +1,252 @@ +use super::ContextError; +use super::{Context, ContextLock}; +use crate::mode::mock::farm::context::MockId; +use crate::mode::mock::rest::reject::report_invalid; +use futures::StreamExt; +use jortestkit::web::api_token::TokenError; +use jortestkit::web::api_token::{APIToken, APITokenManager, API_TOKEN_HEADER}; +use rustls::KeyLogFile; +use std::convert::Infallible; +use std::fs::{self, File}; +use std::path::Path; +use std::sync::Arc; +use thiserror::Error; +use tokio::net::TcpListener; +use tokio_rustls::TlsAcceptor; +use valgrind::Protocol; +use vit_servicing_station_lib::v0::result::HandlerResult; +use warp::http::header::{HeaderMap, HeaderValue}; +use warp::hyper::service::make_service_fn; +use warp::{reject::Reject, Filter, Rejection, Reply}; + +impl warp::reject::Reject for ContextError {} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Error)] +pub enum Error { + #[error("cannot parse uuid")] + CannotParseUuid(#[from] uuid::Error), + #[error(transparent)] + IO(#[from] std::io::Error), + #[error(transparent)] + Hyper(#[from] hyper::Error), + #[error(transparent)] + Rusls(#[from] rustls::Error), + #[error(transparent)] + Crypto(#[from] chain_crypto::hash::Error), + #[error("invalid tls certificate")] + InvalidCertificate, + #[error("invalid tls key")] + InvalidKey, +} + +impl Reject for Error {} + +pub async fn start_rest_server(context: ContextLock) -> Result<(), Error> { + let is_token_enabled = context.lock().unwrap().api_token().is_some(); + let address = context.lock().unwrap().address(); + let protocol = context.lock().unwrap().protocol(); + let with_context = warp::any().map(move || context.clone()); + + let mut default_headers = HeaderMap::new(); + default_headers.insert("Access-Control-Allow-Origin", HeaderValue::from_static("*")); + default_headers.insert("vary", HeaderValue::from_static("Origin")); + + let root = warp::path!("api" / ..); + + let v0 = { + let root = warp::path!("v0" / ..); + + let active = warp::path!("active") + .and(warp::get()) + .and(with_context.clone()) + .and_then(get_active_mocks) + .boxed(); + + let shutdown = warp::path!("shutdown" / String) + .and(warp::post()) + .and(with_context.clone()) + .and_then(shutdown_mock) + .with(warp::reply::with::headers(default_headers.clone())) + .boxed(); + + let start = { + let root = warp::path!("start" / ..); + + let start_mock_on_random_port = warp::path!(String) + .and(warp::post()) + .and(with_context.clone()) + .and_then(start_mock_on_random_port) + .with(warp::reply::with::headers(default_headers.clone())) + .boxed(); + + let start_mock = warp::path!(String / u16) + .and(warp::post()) + .and(with_context.clone()) + .and_then(start_mock) + .with(warp::reply::with::headers(default_headers.clone())) + .boxed(); + + root.and(start_mock_on_random_port.or(start_mock)).boxed() + }; + + root.and(active.or(shutdown).or(start)).boxed() + }; + + let api_token_filter = if is_token_enabled { + warp::header::header(API_TOKEN_HEADER) + .and(with_context.clone()) + .and_then(authorize_token) + .and(warp::any()) + .untuple_one() + .boxed() + } else { + warp::any().boxed() + }; + + let cors = warp::cors() + .allow_any_origin() + .allow_methods((vec!["GET", "POST", "OPTIONS", "PUT", "PATCH"]).clone()) + .allow_headers(vec!["content-type"]) + .build(); + + let api = root + .and(api_token_filter.and(v0)) + .recover(report_invalid) + .with(cors); + + match protocol { + Protocol::Https(certs) => { + let tls_cfg = { + let cert = load_cert(&certs.cert_path)?; + let key = load_private_key(&certs.key_path)?; + let mut cfg = rustls::ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert, key)?; + + cfg.key_log = Arc::new(KeyLogFile::new()); + cfg.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; + Arc::new(cfg) + }; + + let tls_acceptor = TlsAcceptor::from(tls_cfg); + let arc_acceptor = Arc::new(tls_acceptor); + + let listener = + tokio_stream::wrappers::TcpListenerStream::new(TcpListener::bind(&address).await?); + + let incoming = + hyper::server::accept::from_stream(listener.filter_map(|socket| async { + match socket { + Ok(stream) => match arc_acceptor.clone().accept(stream).await { + Ok(val) => Some(Ok::<_, hyper::Error>(val)), + Err(e) => { + tracing::warn!("handshake failed {}", e); + None + } + }, + Err(e) => { + tracing::error!("tcp socket outer err: {}", e); + None + } + } + })); + + let svc = warp::service(api); + let service = make_service_fn(move |_| { + let svc = svc.clone(); + async move { Ok::<_, Infallible>(svc) } + }); + + let server = hyper::Server::builder(incoming).serve(service); + + println!("serving at: https://{}", address); + Ok(server.await?) + } + Protocol::Http => { + println!("serving at: http://{}", address); + warp::serve(api).bind(address).await; + Ok(()) + } + } +} + +fn load_cert(filename: &Path) -> Result, Error> { + let certfile = fs::File::open(filename)?; + let mut reader = std::io::BufReader::new(certfile); + + match rustls_pemfile::read_one(&mut reader)? { + Some(rustls_pemfile::Item::X509Certificate(cert)) => Ok(vec![rustls::Certificate(cert)]), + Some(rustls_pemfile::Item::RSAKey(_)) | Some(rustls_pemfile::Item::PKCS8Key(_)) => { + // TODO: a more specific error could be useful (ExpectedCertFoundKey?) + Err(Error::InvalidCertificate) + } + // not a pemfile + None => Err(Error::InvalidCertificate), + } +} + +fn load_private_key(filename: &Path) -> Result { + let keyfile = File::open(filename)?; + let mut reader = std::io::BufReader::new(keyfile); + + match rustls_pemfile::read_one(&mut reader)? { + Some(rustls_pemfile::Item::RSAKey(key)) => Ok(rustls::PrivateKey(key)), + Some(rustls_pemfile::Item::PKCS8Key(key)) => Ok(rustls::PrivateKey(key)), + None | Some(rustls_pemfile::Item::X509Certificate(_)) => Err(Error::InvalidKey), + } +} + +pub async fn get_active_mocks(context: ContextLock) -> Result { + let context_lock = context.lock().unwrap(); + let active = context_lock.get_active_mocks(); + Ok(HandlerResult(Ok(Some(active)))) +} + +pub async fn shutdown_mock(id: MockId, context: ContextLock) -> Result { + let mut context_lock = context.lock().unwrap(); + context_lock.shutdown_mock(id.clone())?; + Ok(HandlerResult(Ok(format!( + "mock environment '{}' was shutdown successfully", + id + )))) +} + +pub async fn start_mock_on_random_port( + id: MockId, + context: ContextLock, +) -> Result { + let mut context_lock = context.lock().unwrap(); + let port = context_lock.start_mock_on_random_port(id)?; + Ok(HandlerResult(Ok(port))) +} + +pub async fn start_mock( + id: MockId, + port: u16, + context: ContextLock, +) -> Result { + let mut context_lock = context.lock().unwrap(); + let port = context_lock.start_mock(id, port)?; + Ok(HandlerResult(Ok(port))) +} + +pub async fn authorize_token( + token: String, + context: Arc>, +) -> Result<(), Rejection> { + let api_token = APIToken::from_string(token).map_err(warp::reject::custom)?; + + if context.lock().unwrap().api_token().is_none() { + return Ok(()); + } + + let manager = APITokenManager::new(context.lock().unwrap().api_token().unwrap()) + .map_err(warp::reject::custom)?; + + if !manager.is_token_valid(api_token) { + return Err(warp::reject::custom(TokenError::UnauthorizedToken)); + } + Ok(()) +} diff --git a/vitup/src/mode/mock/mod.rs b/vitup/src/mode/mock/mod.rs index c9ea879f..c55ed871 100644 --- a/vitup/src/mode/mock/mod.rs +++ b/vitup/src/mode/mock/mod.rs @@ -1,6 +1,7 @@ mod config; mod congestion; mod context; +pub mod farm; mod ledger_state; mod logger; mod mock_state; diff --git a/vitup/src/mode/mock/rest/mod.rs b/vitup/src/mode/mock/rest/mod.rs index ded75728..8f3f6bb4 100644 --- a/vitup/src/mode/mock/rest/mod.rs +++ b/vitup/src/mode/mock/rest/mod.rs @@ -42,7 +42,7 @@ use voting_hir::VoterHIR; use warp::http::header::{HeaderMap, HeaderValue}; use warp::hyper::service::make_service_fn; use warp::{reject::Reject, Filter, Rejection, Reply}; -mod reject; +pub mod reject; use reject::{report_invalid, ForcedErrorCode, GeneralException, InvalidBatch}; diff --git a/vitup/vole.trace b/vitup/vole.trace deleted file mode 100644 index 56e7d1eb..00000000 --- a/vitup/vole.trace +++ /dev/null @@ -1,2 +0,0 @@ -May 25 12:41:27.688 DEBUG warp::filter::service: rejected: Rejection(MethodNotAllowed) -May 25 12:41:48.802 DEBUG warp::filter::service: rejected: Rejection(NotFound)