|
|
|
|
@@ -29,7 +29,9 @@ use openssl::sign::Signer;
|
|
|
|
|
use openssl::sign::Verifier;
|
|
|
|
|
use reqwest::Client as rClient;
|
|
|
|
|
use std::fs;
|
|
|
|
|
use std::net::SocketAddr;
|
|
|
|
|
use std::time::Instant;
|
|
|
|
|
use url::Url;
|
|
|
|
|
|
|
|
|
|
const LOCKTIME_THRESHOLD: i64 = 5000000;
|
|
|
|
|
const VERSION: &str = "0.0.2";
|
|
|
|
|
@@ -331,8 +333,14 @@ async fn main_result(cfg: &MyConfig, network_params: &NetworkParams) -> Result<(
|
|
|
|
|
stmt.bind((2, Value::String(txid.clone()))).unwrap();
|
|
|
|
|
let _ = stmt.next();
|
|
|
|
|
}
|
|
|
|
|
let _ = send_stats_report(cfg, bcinfo).await;
|
|
|
|
|
let _ = calculate_stats(&db, network_params.db_field.clone()).await;
|
|
|
|
|
if let Err(e) = send_stats_report(cfg, bcinfo).await {
|
|
|
|
|
// Never discard silently: a failing report is otherwise
|
|
|
|
|
// invisible in the logs and can go unnoticed for a long time.
|
|
|
|
|
warn!("send_stats_report failed: {e}");
|
|
|
|
|
}
|
|
|
|
|
if let Err(e) = calculate_stats(&db, network_params.db_field.clone()).await {
|
|
|
|
|
warn!("calculate_stats failed: {e}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(erx) => {
|
|
|
|
|
error!("impossible to get client: {}, retrying on next block", erx);
|
|
|
|
|
@@ -422,6 +430,76 @@ ON CONFLICT(chain) DO UPDATE SET
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
/// Parse the `(host, port)` pair from a base URL like `https://host[:port]`.
|
|
|
|
|
///
|
|
|
|
|
/// Falls back to the scheme's well-known default port (443 for `https`,
|
|
|
|
|
/// 80 for plain `http`), or to 443 when the scheme is unknown.
|
|
|
|
|
fn parse_host_port(base_url: &str) -> Option<(String, u16)> {
|
|
|
|
|
let url = Url::parse(base_url).ok()?;
|
|
|
|
|
let host = url
|
|
|
|
|
.host_str()?
|
|
|
|
|
.trim_start_matches('[')
|
|
|
|
|
.trim_end_matches(']')
|
|
|
|
|
.to_string();
|
|
|
|
|
let port = url.port_or_known_default().unwrap_or(443);
|
|
|
|
|
Some((host, port))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Resolve `host:port` and return the first IPv6 (AAAA) address, if any.
|
|
|
|
|
///
|
|
|
|
|
/// Returns `None` when the host has no IPv6 address.
|
|
|
|
|
async fn resolve_first_ipv6(host: &str, port: u16) -> Option<SocketAddr> {
|
|
|
|
|
use std::net::ToSocketAddrs;
|
|
|
|
|
let host = host.to_string();
|
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
|
|
|
format!("{}:{}", host, port)
|
|
|
|
|
.to_socket_addrs()
|
|
|
|
|
.ok()
|
|
|
|
|
.and_then(|mut addrs| addrs.find(|a| a.is_ipv6()))
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.ok()
|
|
|
|
|
.flatten()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Build the HTTP client used for welist reports.
|
|
|
|
|
///
|
|
|
|
|
/// When `BAL_PUSHER_PREFER_IPV6` is truthy, the welist host is resolved and
|
|
|
|
|
/// the client is pinned to its first IPv6 address (the original hostname is
|
|
|
|
|
/// still used for the `Host` header and TLS SNI). This works around networks
|
|
|
|
|
/// where the IPv4 route to the welist host is broken while IPv6 works: the
|
|
|
|
|
/// default connector may otherwise pick the broken family and the request
|
|
|
|
|
/// stalls. When the variable is unset (the default), behavior is unchanged.
|
|
|
|
|
async fn welist_http_client(welist_url: &str) -> rClient {
|
|
|
|
|
let prefer_ipv6 = env::var("BAL_PUSHER_PREFER_IPV6")
|
|
|
|
|
.unwrap_or("false".to_string())
|
|
|
|
|
.parse::<bool>()
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
if !prefer_ipv6 {
|
|
|
|
|
return rClient::new();
|
|
|
|
|
}
|
|
|
|
|
let (host, port) = match parse_host_port(welist_url) {
|
|
|
|
|
Some(hp) => hp,
|
|
|
|
|
None => {
|
|
|
|
|
warn!("BAL_PUSHER_PREFER_IPV6: cannot parse '{welist_url}', using default resolver");
|
|
|
|
|
return rClient::new();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
match resolve_first_ipv6(&host, port).await {
|
|
|
|
|
Some(addr) => {
|
|
|
|
|
debug!("BAL_PUSHER_PREFER_IPV6: pinning {host} to {addr}");
|
|
|
|
|
rClient::builder()
|
|
|
|
|
.resolve(&host, addr)
|
|
|
|
|
.build()
|
|
|
|
|
.unwrap_or_else(|_| rClient::new())
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
debug!("BAL_PUSHER_PREFER_IPV6: no IPv6 address for {host}, using default resolver");
|
|
|
|
|
rClient::new()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn send_stats_report(
|
|
|
|
|
cfg: &MyConfig,
|
|
|
|
|
bcinfo: GetBlockchainInfoResult,
|
|
|
|
|
@@ -437,7 +515,7 @@ async fn send_stats_report(
|
|
|
|
|
);
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
let client = rClient::new();
|
|
|
|
|
let client = welist_http_client(&welist_url).await;
|
|
|
|
|
let url = format!("{}/ping", welist_url);
|
|
|
|
|
debug!("welist url: {}", url);
|
|
|
|
|
let chain = bcinfo.chain.to_string().to_lowercase();
|
|
|
|
|
@@ -724,3 +802,45 @@ fn seq_to_str(seq: &Vec<u8>) -> String {
|
|
|
|
|
}
|
|
|
|
|
"Unknown".to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_host_port_https_default_port() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
parse_host_port("https://welist.bitcoin-after.life"),
|
|
|
|
|
Some(("welist.bitcoin-after.life".to_string(), 443))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_host_port_explicit_port_and_path() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
parse_host_port("https://example.com:8443/ping"),
|
|
|
|
|
Some(("example.com".to_string(), 8443))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_host_port_http_default_port() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
parse_host_port("http://example.com"),
|
|
|
|
|
Some(("example.com".to_string(), 80))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_host_port_ipv6_literal_brackets_stripped() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
parse_host_port("https://[2a13:2c0::1]:443"),
|
|
|
|
|
Some(("2a13:2c0::1".to_string(), 443))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_host_port_invalid_url() {
|
|
|
|
|
assert_eq!(parse_host_port("not a url"), None);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|