duniter-gva/src/lib.rs

499 lines
16 KiB
Rust
Raw Normal View History

2021-04-22 16:44:17 +02:00
// Copyright (C) 2020 Éloïs SANCHEZ.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#![deny(
clippy::unwrap_used,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unstable_features,
unused_import_braces
)]
mod anti_spam;
mod warp_;
use async_graphql::http::GraphQLPlaygroundConfig;
2021-05-20 14:30:47 +02:00
use duniter_core::common::{currency_params::CurrencyParameters, prelude::*};
2021-04-22 16:44:17 +02:00
use duniter_core::dbs::databases::txs_mp_v2::TxsMpV2DbReadable;
use duniter_core::dbs::prelude::*;
use duniter_core::dbs::{kv_typed::prelude::*, FileBackend};
2021-05-20 14:30:47 +02:00
use duniter_core::documents::transaction::TransactionDocumentV10;
2021-04-22 16:44:17 +02:00
use duniter_core::global::AsyncAccessor;
use duniter_core::mempools::Mempools;
#[cfg(not(test))]
use duniter_core::module::public_ips::get_public_ips;
2021-05-20 14:30:47 +02:00
use duniter_core::{block::DubpBlockV10, crypto::hashs::Hash};
use duniter_core::{
common::crypto::keys::{ed25519::PublicKey, KeyPair as _},
crypto::keys::ed25519::Ed25519KeyPair,
};
use duniter_core::{conf::DuniterMode, module::Endpoint};
use duniter_gva_conf::GvaConf;
2021-04-22 16:44:17 +02:00
use duniter_gva_db::*;
use duniter_gva_gql::{GvaSchema, QueryContext};
use duniter_gva_indexer::{get_gva_db_ro, get_gva_db_rw};
use futures::{StreamExt, TryStreamExt};
use std::{convert::Infallible, path::Path};
#[cfg(test)]
use tests::get_public_ips;
2021-04-22 16:44:17 +02:00
use warp::{http::Response as HttpResponse, Filter as _, Rejection};
const PLAYGROUND_SUB_PATH: &str = "playground";
const SUBSCRIPTION_SUB_PATH: &str = "subscription";
2021-04-22 16:44:17 +02:00
#[derive(Debug)]
pub struct GvaModule {
2021-05-05 23:35:58 +02:00
conf: GvaConf,
2021-04-22 16:44:17 +02:00
currency: String,
dbs_pool: fast_threadpool::ThreadPoolAsyncHandler<SharedDbs<FileBackend>>,
gva_db_ro: &'static GvaV1DbRo<FileBackend>,
mempools: Mempools,
mode: DuniterMode,
self_keypair: Ed25519KeyPair,
software_version: &'static str,
}
#[async_trait::async_trait]
impl duniter_core::module::DuniterModule for GvaModule {
const INDEX_BLOCKS: bool = true;
const MODULE_NAME: &'static str = "gva";
type Conf = GvaConf;
2021-04-22 16:44:17 +02:00
fn apply_block(
block: &DubpBlockV10,
_conf: &duniter_core::conf::DuniterCoreConf,
2021-05-17 15:13:14 +02:00
currency_params: CurrencyParameters,
2021-04-22 16:44:17 +02:00
profile_path_opt: Option<&Path>,
) -> KvResult<()> {
if let Some(profile_path) = profile_path_opt {
let gva_db = get_gva_db_rw(profile_path_opt);
duniter_gva_indexer::apply_block_blocks_chunk(&block, &gva_db, profile_path)?;
duniter_gva_indexer::apply_block(&block, currency_params, gva_db)
} else {
let gva_db = get_gva_db_rw(profile_path_opt);
duniter_gva_indexer::apply_block(&block, currency_params, gva_db)
}
2021-04-22 16:44:17 +02:00
}
fn revert_block(
block: &DubpBlockV10,
_conf: &duniter_core::conf::DuniterCoreConf,
2021-05-17 15:13:14 +02:00
currency_params: CurrencyParameters,
2021-04-22 16:44:17 +02:00
profile_path_opt: Option<&Path>,
) -> KvResult<()> {
if let Some(profile_path) = profile_path_opt {
let gva_db = get_gva_db_rw(profile_path_opt);
duniter_gva_indexer::revert_block_blocks_chunk(&block, &gva_db, profile_path)?;
duniter_gva_indexer::revert_block(&block, currency_params, gva_db)
} else {
let gva_db = get_gva_db_rw(profile_path_opt);
duniter_gva_indexer::revert_block(&block, currency_params, gva_db)
}
2021-04-22 16:44:17 +02:00
}
async fn init(
conf: Self::Conf,
core_conf: &duniter_core::conf::DuniterCoreConf,
2021-04-22 16:44:17 +02:00
currency: &str,
dbs_pool: &fast_threadpool::ThreadPoolAsyncHandler<SharedDbs<FileBackend>>,
mempools: Mempools,
mode: duniter_core::conf::DuniterMode,
profile_path_opt: Option<&Path>,
software_version: &'static str,
) -> anyhow::Result<(Self, Vec<duniter_core::module::Endpoint>)> {
let self_keypair = core_conf.self_key_pair.clone();
let endpoints = Self::gen_endpoints(&conf).await?;
2021-04-22 16:44:17 +02:00
Ok((
GvaModule {
2021-05-05 23:35:58 +02:00
conf,
2021-04-22 16:44:17 +02:00
currency: currency.to_owned(),
dbs_pool: dbs_pool.to_owned(),
gva_db_ro: get_gva_db_ro(profile_path_opt),
mempools,
mode,
2021-05-05 23:35:58 +02:00
self_keypair,
2021-04-22 16:44:17 +02:00
software_version,
},
endpoints,
))
}
async fn start(self) -> anyhow::Result<()> {
// Do not start GVA server on js tests
if std::env::var_os("DUNITER_JS_TESTS") != Some("yes".into()) {
let GvaModule {
conf,
currency,
dbs_pool,
gva_db_ro,
mempools,
mode,
self_keypair,
software_version,
} = self;
if let DuniterMode::Start = mode {
if conf.enabled {
2021-04-22 16:44:17 +02:00
GvaModule::start_inner(
conf,
currency,
dbs_pool,
gva_db_ro,
mempools,
self_keypair,
software_version,
)
.await
}
}
}
Ok(())
}
// Needed for BMA only, will be removed when the migration is complete.
fn get_transactions_history_for_bma(
dbs_pool: &fast_threadpool::ThreadPoolSyncHandler<SharedDbs<FileBackend>>,
profile_path_opt: Option<&Path>,
pubkey: PublicKey,
) -> KvResult<Option<duniter_core::module::TxsHistoryForBma>> {
let gva_db = get_gva_db_ro(profile_path_opt);
let duniter_gva_dbs_reader::txs_history::TxsHistory {
sent,
received,
sending,
pending,
} = dbs_pool
.execute(move |dbs| {
duniter_gva_dbs_reader::txs_history::get_transactions_history_for_bma(
gva_db,
&dbs.txs_mp_db,
pubkey,
)
})
.expect("dbs pool disconnected")?;
Ok(Some(duniter_core::module::TxsHistoryForBma {
sent: sent
.into_iter()
.map(
|GvaTxDbV1 {
tx,
written_block,
written_time,
}| (tx, written_block, written_time),
)
.collect(),
received: received
.into_iter()
.map(
|GvaTxDbV1 {
tx,
written_block,
written_time,
}| (tx, written_block, written_time),
)
.collect(),
sending,
pending,
}))
}
// Needed for BMA only, will be removed when the migration is complete.
fn get_tx_by_hash(
dbs_pool: &fast_threadpool::ThreadPoolSyncHandler<SharedDbs<FileBackend>>,
hash: Hash,
profile_path_opt: Option<&Path>,
) -> KvResult<Option<(TransactionDocumentV10, Option<BlockNumber>)>> {
let gva_db = get_gva_db_ro(profile_path_opt);
dbs_pool
.execute(move |dbs| {
if let Some(tx) = dbs
.txs_mp_db
.txs()
.get(&duniter_core::dbs::HashKeyV2(hash))?
{
Ok(Some((tx.doc, None)))
2021-04-22 16:44:17 +02:00
} else if let Some(tx_db) = gva_db.txs().get(&duniter_core::dbs::HashKeyV2(hash))? {
Ok(Some((tx_db.tx, Some(tx_db.written_block.number))))
} else {
Ok(None)
}
})
.expect("dbs pool disconnected")
}
}
impl GvaModule {
async fn start_inner(
conf: GvaConf,
currency: String,
dbs_pool: fast_threadpool::ThreadPoolAsyncHandler<SharedDbs<FileBackend>>,
gva_db_ro: &'static GvaV1DbRo<FileBackend>,
mempools: Mempools,
self_keypair: Ed25519KeyPair,
software_version: &'static str,
) {
log::info!("GvaServer::start: conf={:?}", conf);
// Create BdaExecutor and GvaSchema
2021-04-22 16:44:17 +02:00
let self_pubkey = self_keypair.public_key();
duniter_bda::set_bda_executor(
2021-04-22 16:44:17 +02:00
currency.clone(),
AsyncAccessor::new(),
dbs_pool.clone(),
duniter_gva_dbs_reader::create_dbs_reader(gva_db_ro),
self_keypair,
software_version,
mempools.txs,
);
let gva_schema = duniter_gva_gql::build_schema_with_data(
duniter_gva_gql::GvaSchemaData {
cm_accessor: AsyncAccessor::new(),
dbs_reader: duniter_gva_dbs_reader::create_dbs_reader(gva_db_ro),
dbs_pool,
server_meta_data: duniter_gva_gql::ServerMetaData {
currency,
self_pubkey,
software_version,
},
txs_mempool: mempools.txs,
},
true,
);
// Create warp server routes
let gva_route = warp_::gva_route(
2021-04-22 16:44:17 +02:00
&conf,
gva_schema.clone(),
async_graphql::http::MultipartOptions::default(),
);
let gva_playground_route = warp_::gva_playground_route(&conf);
let gva_subscription_route = warp_::gva_subscription_route(&conf, gva_schema.clone());
2021-04-22 16:44:17 +02:00
// Define recover function
let recover_func = |err: Rejection| async move {
if let Some(warp_::BadRequest(err)) = err.find() {
return Ok::<_, Infallible>(warp::reply::with_status(
err.to_string(),
http::StatusCode::BAD_REQUEST,
));
}
Ok(warp::reply::with_status(
"INTERNAL_SERVER_ERROR".to_string(),
http::StatusCode::INTERNAL_SERVER_ERROR,
))
};
2021-04-22 16:44:17 +02:00
// Start warp server
if conf.playground {
Self::run_warp_server(
&conf,
gva_route
.or(gva_subscription_route)
.or(gva_playground_route)
.recover(recover_func),
)
.await
} else {
Self::run_warp_server(
&conf,
gva_route.or(gva_subscription_route).recover(recover_func),
)
.await
}
}
async fn run_warp_server<F>(conf: &GvaConf, routes: F)
where
F: warp::Filter<Error = Infallible> + Clone + Send + Sync + 'static,
F::Extract: warp::Reply,
{
2021-04-22 16:44:17 +02:00
log::info!(
"GVA server listen on http://{}:{}/{}",
conf.ip4,
conf.port,
&conf.path
2021-04-22 16:44:17 +02:00
);
if let Some(ip6) = conf.ip6 {
2021-04-22 16:44:17 +02:00
log::info!(
"GVA server listen on http://[{}]:{}/{}",
2021-04-22 16:44:17 +02:00
ip6,
conf.port,
&conf.path
2021-04-22 16:44:17 +02:00
);
futures::future::join(
warp::serve(routes.clone()).run((conf.ip4, conf.port)),
warp::serve(routes).run((ip6, conf.port)),
2021-04-22 16:44:17 +02:00
)
.await;
} else {
warp::serve(routes).run((conf.ip4, conf.port)).await;
2021-04-22 16:44:17 +02:00
}
log::warn!("GVA server stopped");
}
async fn gen_endpoints(conf: &GvaConf) -> anyhow::Result<Vec<Endpoint>> {
let mut endpoints = Vec::new();
if conf.enabled {
// Fill remote hosh
let remote_hosh = if let Some(remote_host) = conf.remote_host.clone() {
remote_host
} else {
let public_ips = get_public_ips().await;
let mut remote_hosh = String::new();
if let Some(ip4) = public_ips.public_ip4_opt {
remote_hosh.push_str(ip4.to_string().as_str());
remote_hosh.push(' ');
}
if let Some(ip6) = public_ips.public_ip6_opt {
remote_hosh.push_str(&format!("[{}]", ip6.to_string()));
} else if !remote_hosh.is_empty() {
remote_hosh.pop();
} else {
return Err(anyhow::Error::msg(
"Fail to found public IPs, please configure remote_host manually",
));
}
remote_hosh
};
// Fill remote port
let remote_port = conf.get_remote_port();
// Push endpoints
endpoints.push(format!(
"GVA {}{} {} {}",
if remote_port == 443 || conf.remote_tls.unwrap_or_default() {
"S "
} else {
""
},
remote_hosh,
remote_port,
conf.get_remote_path(),
));
}
Ok(endpoints)
}
2021-04-22 16:44:17 +02:00
}
#[cfg(test)]
mod tests {
use super::*;
use duniter_core::mempools::Mempools;
use duniter_core::module::DuniterModule;
use duniter_core::{conf::DuniterCoreConf, module::public_ips::PublicIPs};
2021-04-22 16:44:17 +02:00
use fast_threadpool::{ThreadPool, ThreadPoolConfig};
use std::net::{Ipv4Addr, Ipv6Addr};
2021-04-22 16:44:17 +02:00
use unwrap::unwrap;
static PUBLIC_IPS_MOCK: async_mutex::Mutex<Option<PublicIPs>> = async_mutex::Mutex::new(None);
pub async fn get_public_ips() -> PublicIPs {
let public_ips = *PUBLIC_IPS_MOCK.lock().await;
public_ips.unwrap_or(PublicIPs {
public_ip4_opt: None,
public_ip6_opt: None,
})
}
async fn test_gen_endpoints(
conf: &GvaConf,
public_ips: PublicIPs,
) -> anyhow::Result<Vec<Endpoint>> {
PUBLIC_IPS_MOCK.lock().await.replace(public_ips);
GvaModule::gen_endpoints(&conf).await
}
#[tokio::test]
async fn gen_endpoints() -> anyhow::Result<()> {
let conf = GvaConf {
enabled: true,
..Default::default()
};
// ip4 and ip6 find
let endpoints = test_gen_endpoints(
&conf,
PublicIPs {
public_ip4_opt: Some(Ipv4Addr::UNSPECIFIED),
public_ip6_opt: Some(Ipv6Addr::UNSPECIFIED),
},
)
.await?;
assert_eq!(endpoints, vec!["GVA 0.0.0.0 [::] 30901 gva".to_owned(),]);
// only ip4 find
let endpoints = test_gen_endpoints(
&conf,
PublicIPs {
public_ip4_opt: Some(Ipv4Addr::UNSPECIFIED),
public_ip6_opt: None,
},
)
.await?;
assert_eq!(endpoints, vec!["GVA 0.0.0.0 30901 gva".to_owned(),]);
// only ip6 find
let endpoints = test_gen_endpoints(
&conf,
PublicIPs {
public_ip4_opt: None,
public_ip6_opt: Some(Ipv6Addr::UNSPECIFIED),
},
)
.await?;
assert_eq!(endpoints, vec!["GVA [::] 30901 gva".to_owned(),]);
// No ips find
assert!(test_gen_endpoints(
&conf,
PublicIPs {
public_ip4_opt: None,
public_ip6_opt: None,
},
)
.await
.is_err());
Ok(())
}
2021-04-22 16:44:17 +02:00
#[tokio::test]
#[ignore]
async fn launch_mem_gva() -> anyhow::Result<()> {
let dbs = unwrap!(SharedDbs::mem());
let threadpool = ThreadPool::start(ThreadPoolConfig::default(), dbs);
GvaModule::init(
GvaConf::default(),
&DuniterCoreConf::default(),
2021-04-22 16:44:17 +02:00
"",
&threadpool.into_async_handler(),
Mempools::default(),
duniter_core::conf::DuniterMode::Start,
None,
"test",
)
.await?
2021-04-22 16:44:17 +02:00
.0
.start()
.await?;
Ok(())
}
}