vls_proxy/util/
mod.rs

1mod env_var;
2mod r#macro;
3pub mod observability;
4mod rpc_cookie;
5mod testing;
6mod validation;
7
8pub use rpc_cookie::get_rpc_credentials;
9
10#[cfg(feature = "otlp")]
11mod otlp;
12
13pub use env_var::*;
14pub use testing::*;
15pub use validation::*;
16
17use clap::{arg, AppSettings};
18#[cfg(feature = "main")]
19use clap::{App, Arg, ArgMatches};
20use log::*;
21use std::env;
22use std::fs::File;
23use std::io::{BufRead, BufReader};
24use std::path::Path;
25use time::{macros::format_description, OffsetDateTime};
26use tokio::runtime::{self, Runtime};
27
28pub fn line_filter(line: &str) -> Option<String> {
29    let whitespace_removed = line.trim();
30    if whitespace_removed.is_empty() {
31        return None;
32    }
33    let comment_removed = whitespace_removed.split('#').next()?.trim();
34    if comment_removed.is_empty() {
35        return None;
36    }
37    Some(comment_removed.to_string())
38}
39
40pub fn read_allowlist() -> Vec<String> {
41    if let Ok(allowlist_path) = env::var("REMOTE_SIGNER_ALLOWLIST") {
42        return read_allowlist_path(&allowlist_path);
43    }
44    Vec::new()
45}
46
47pub fn read_allowlist_path(path: &str) -> Vec<String> {
48    let file = File::open(path).expect(format!("open {} failed", path).as_str());
49    let allowlist: Vec<String> =
50        BufReader::new(file).lines().filter_map(|l| line_filter(&l.expect("line"))).collect();
51
52    allowlist
53}
54
55#[cfg(feature = "main")]
56pub fn setup_logging<P: AsRef<Path>>(datadir: P, who: &str, level_arg: &str) {
57    use fern::colors::{Color, ColoredLevelConfig};
58    use std::str::FromStr;
59
60    // Should we support seperate console and file log levels?
61    let level = env::var("RUST_LOG").unwrap_or(level_arg.to_string());
62
63    // file
64    let who_clone = who.to_string();
65    let logfile = datadir.as_ref().join(format!("{}.log", who));
66    let file_config = fern::Dispatch::new()
67        .format(move |out, message, record| {
68            out.finish(format_args!(
69                "[{} {}/{} {}] {}",
70                tstamp(),
71                who_clone,
72                record.target(),
73                record.level(),
74                message
75            ))
76        })
77        .level(log::LevelFilter::from_str(&level).expect("level"))
78        .level_for("h2", log::LevelFilter::Info)
79        .chain(fern::log_file(logfile).expect("file log config"));
80
81    // console
82    let who_clone = who.to_string();
83    let colors = ColoredLevelConfig::new().info(Color::Green).error(Color::Red).warn(Color::Yellow);
84    let console_config = fern::Dispatch::new()
85        .format(move |out, message, record| {
86            out.finish(format_args!(
87                "[{} {}/{} {}] {}",
88                tstamp(),
89                who_clone,
90                record.target(),
91                colors.color(record.level()),
92                message
93            ))
94        })
95        .level(log::LevelFilter::from_str(&level).expect("level"))
96        .level_for("h2", log::LevelFilter::Info)
97        .chain(std::io::stdout());
98
99    fern::Dispatch::new().chain(console_config).chain(file_config).apply().expect("log config");
100}
101
102#[cfg(feature = "main")]
103pub fn add_hsmd_args(app: App) -> App {
104    app.setting(AppSettings::NoAutoVersion)
105        .arg(
106            Arg::new("dev-disconnect")
107                .help("ignored dev flag")
108                .long("dev-disconnect")
109                .takes_value(true),
110        )
111        .arg(Arg::new("developer").long("developer").help("ignored dev flag"))
112        .arg(Arg::new("log-io").long("log-io").help("ignored dev flag"))
113        .arg(arg!(--version "show a dummy version"))
114        .arg(Arg::new("git-desc").long("git-desc").help("print git desc version and exit"))
115}
116
117#[cfg(feature = "main")]
118pub fn handle_hsmd_version(matches: &ArgMatches) -> bool {
119    if matches.is_present("version") {
120        // Pretend to be the right version, given to us by an env var
121        let version = vls_cln_version();
122        println!("{}", version);
123        true
124    } else {
125        false
126    }
127}
128
129pub fn create_runtime(thread_name: &str) -> Runtime {
130    let thrname = thread_name.to_string();
131    std::thread::spawn(|| {
132        runtime::Builder::new_multi_thread()
133            .enable_all()
134            .thread_name(thrname)
135            .worker_threads(2) // for debugging
136            .build()
137    })
138    .join()
139    .expect("runtime join")
140    .expect("runtime")
141}
142
143/// Determine if we should auto approve payments
144pub fn should_auto_approve() -> bool {
145    if compare_env_var("VLS_PERMISSIVE", "1") {
146        warn!("VLS_PERMISSIVE: ALL INVOICES, KEYSENDS, AND PAYMENTS AUTOMATICALLY APPROVED");
147        return true;
148    }
149
150    if compare_env_var("VLS_AUTOAPPROVE", "1") {
151        warn!("VLS_AUTOAPPROVE: ALL INVOICES, KEYSENDS, AND PAYMENTS AUTOMATICALLY APPROVED");
152        return true;
153    }
154
155    info!("VLS_ENFORCING: ALL INVOICES, KEYSENDS, AND PAYMENTS REQUIRE APPROVAL");
156    false
157}
158
159/// Abort on panic.
160/// Use this instead of `panic = abort` in Cargo.toml, which doesn't show
161/// nice backtraces.
162pub fn abort_on_panic() {
163    let old = std::panic::take_hook();
164    std::panic::set_hook(Box::new(move |info| {
165        old(info);
166        std::process::abort();
167    }));
168}
169
170// Would prefer to use now_local but https://rustsec.org/advisories/RUSTSEC-2020-0071
171// Also, https://time-rs.github.io/api/time/struct.OffsetDateTime.html#method.now_local
172#[cfg(feature = "main")]
173pub fn tstamp() -> String {
174    OffsetDateTime::now_utc()
175        .format(format_description!(
176            "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]"
177        ))
178        .expect("formatted tstamp")
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use std::io::Write;
185
186    #[test]
187    fn line_filter_test() {
188        assert_eq!(line_filter("#"), None);
189        assert_eq!(
190            line_filter("tb1qhetd7l0rv6kca6wvmt25ax5ej05eaat9q29z7z # comment"),
191            Some("tb1qhetd7l0rv6kca6wvmt25ax5ej05eaat9q29z7z".to_string())
192        );
193        assert_eq!(line_filter("   "), None);
194        assert_eq!(line_filter("   #   "), None);
195        assert_eq!(
196            line_filter("   tb1qhetd7l0rv6kca6wvmt25ax5ej05eaat9q29z7z   "),
197            Some("tb1qhetd7l0rv6kca6wvmt25ax5ej05eaat9q29z7z".to_string())
198        );
199    }
200
201    #[test]
202    fn read_allowlist_test() {
203        let test_file_content = "\
204        # Sample Allowlist
205        tb1qhetd7l0rv6kca6wvmt25ax5ej05eaat9q29z7z
206        tb1qexampleaddress1234567890123456789012345678
207
208        # Another comment line after blank line
209    ";
210        let mut temp_file = tempfile::NamedTempFile::new().unwrap();
211        write!(temp_file, "{}", test_file_content).unwrap();
212        let allowlist = read_allowlist_path(temp_file.path().to_str().unwrap());
213        assert_eq!(
214            allowlist,
215            vec![
216                "tb1qhetd7l0rv6kca6wvmt25ax5ej05eaat9q29z7z".to_string(),
217                "tb1qexampleaddress1234567890123456789012345678".to_string(),
218            ]
219        );
220        temp_file.close().unwrap();
221    }
222}