152 lines
4.9 KiB
Rust
152 lines
4.9 KiB
Rust
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<PluginState, tokio::io::Stdin, tokio::io::Stdout>,
|
|
) -> Result<ClnrestOptions, anyhow::Error> {
|
|
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<CorsLayer, anyhow::Error> {
|
|
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::<Result<Vec<HeaderValue>, anyhow::Error>>()?;
|
|
Ok(CorsLayer::new()
|
|
.allow_origin(origins)
|
|
.allow_methods(Any)
|
|
.allow_headers(Any))
|
|
}
|
|
}
|