use std::{ env, net::{SocketAddr, ToSocketAddrs}, path::PathBuf, }; use anyhow::anyhow; use axum::http::HeaderValue; use cln_plugin::{ options::{ ConfigOption, DefaultStringArrayConfigOption, DefaultStringConfigOption, IntegerConfigOption, StringConfigOption, }, ConfiguredPlugin, }; use tower_http::cors::{Any, CorsLayer}; use crate::PluginState; pub const OPT_CLNREST_PORT: IntegerConfigOption = ConfigOption::new_i64_no_default("clnrest-port", "REST server port to listen"); pub const OPT_CLNREST_CERTS: StringConfigOption = ConfigOption::new_str_no_default("clnrest-certs", "Path for certificates (for https)"); pub const OPT_CLNREST_PROTOCOL: DefaultStringConfigOption = ConfigOption::new_str_with_default("clnrest-protocol", "https", "REST server protocol"); pub const OPT_CLNREST_HOST: DefaultStringConfigOption = ConfigOption::new_str_with_default("clnrest-host", "127.0.0.1", "REST server host"); pub const OPT_CLNREST_CORS: DefaultStringArrayConfigOption = ConfigOption::new_str_arr_with_default( "clnrest-cors-origins", "*", "Cross origin resource sharing origins", ); pub const OPT_CLNREST_CSP: DefaultStringConfigOption = ConfigOption::new_str_with_default( "clnrest-csp", "default-src 'self'; font-src 'self'; img-src 'self' data:; frame-src 'self'; \ style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline';", "Content security policy (CSP) for the server", ); pub const OPT_CLNREST_SWAGGER: DefaultStringConfigOption = ConfigOption::new_str_with_default("clnrest-swagger-root", "/", "Root path for Swagger UI"); pub const SWAGGER_FALLBACK: &str = "/swagger-ui"; pub enum ClnrestProtocol { Https, Http, } pub struct ClnrestOptions { pub certs: PathBuf, pub protocol: ClnrestProtocol, pub address_str: String, pub address: SocketAddr, pub cors: CorsLayer, pub csp: String, pub swagger: String, } pub async fn parse_options( plugin: &ConfiguredPlugin, ) -> Result { let port = if let Some(p) = plugin.option(&OPT_CLNREST_PORT)? { if !(1024..=65535).contains(&p) { return Err(anyhow!( "`clnrest-port` {}, should be a valid available port between 1024 and 65535.", p )); } p as u16 } else { log::info!("`clnrest-port` option is not configured"); return Err(anyhow!("`clnrest-port` option is not configured")); }; let protocol = match plugin.option(&OPT_CLNREST_PROTOCOL)? { p if p.eq_ignore_ascii_case("https") => ClnrestProtocol::Https, p if p.eq_ignore_ascii_case("http") => ClnrestProtocol::Http, _ => { return Err(anyhow!("`clnrest-protocol` can either be http or https.")); } }; let address_str = format!("{}:{}", plugin.option(&OPT_CLNREST_HOST)?, port); let address: SocketAddr = match plugin.option(&OPT_CLNREST_HOST)? { i if i.eq("localhost") => address_str .to_socket_addrs()? .next() .ok_or(anyhow!("No address found for localhost"))?, _ => { if let Ok(addr) = address_str.parse() { addr } else { return Err(anyhow!("`clnrest-host` should be a valid IP.")); } } }; let cors = create_cors_layer(&plugin.option(&OPT_CLNREST_CORS)?)?; let certs = if let Some(cert_opt) = plugin.option(&OPT_CLNREST_CERTS)? { PathBuf::from(cert_opt) } else { env::current_dir()? }; let csp = plugin.option(&OPT_CLNREST_CSP)?; let swagger = match plugin.option(&OPT_CLNREST_SWAGGER)? { swag if !swag.starts_with('/') => { return Err(anyhow!("`clnrest-swagger-root` must start with `/`")) } swag => swag, }; Ok(ClnrestOptions { certs, protocol, address_str, address, cors, csp, swagger, }) } fn create_cors_layer(allowed_origins: &[String]) -> Result { if allowed_origins.is_empty() { return Err(anyhow!("`clnrest-cors-origins` must not be empty!")); } if allowed_origins.first().unwrap() == "*" { Ok(CorsLayer::new() .allow_origin(Any) .allow_methods(Any) .allow_headers(Any)) } else { let origins = allowed_origins .iter() .map(|header_val| { HeaderValue::from_str(header_val).map_err(|err| { anyhow!( "Could not parse CORS header value `{}`: {}", header_val, err ) }) }) .collect::, anyhow::Error>>()?; Ok(CorsLayer::new() .allow_origin(origins) .allow_methods(Any) .allow_headers(Any)) } }