clnrest: add more valid request and response types
Changelog-Added: clnrest can now return successful responses as xml, yaml, or form-encoded in addition to json defined in the 'Accept' header. The same goes for request types defined in the 'Content-type' header.
This commit is contained in:
committed by
ShahanaFarooqui
parent
2e7181d04f
commit
bf37d41c7e
@@ -27,6 +27,7 @@ pub enum AppError {
|
||||
Forbidden(RpcError),
|
||||
NotFound(RpcError),
|
||||
InternalServerError(RpcError),
|
||||
NotAcceptable(RpcError),
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
@@ -36,6 +37,7 @@ impl IntoResponse for AppError {
|
||||
AppError::Forbidden(err) => (StatusCode::FORBIDDEN, err),
|
||||
AppError::NotFound(err) => (StatusCode::NOT_FOUND, err),
|
||||
AppError::InternalServerError(err) => (StatusCode::INTERNAL_SERVER_ERROR, err),
|
||||
AppError::NotAcceptable(err) => (StatusCode::NOT_ACCEPTABLE, err),
|
||||
};
|
||||
|
||||
let body = Json(json!(error_message));
|
||||
@@ -83,19 +85,38 @@ fn process_help_response(help_response: serde_json::Value) -> String {
|
||||
processed_html_res
|
||||
}
|
||||
|
||||
/* Example for swagger ui */
|
||||
#[derive(utoipa::ToSchema)]
|
||||
#[allow(non_camel_case_types)]
|
||||
struct newaddr {
|
||||
#[schema(example = "p2tr")]
|
||||
#[allow(dead_code)]
|
||||
addresstype: String,
|
||||
}
|
||||
|
||||
/* Example for swagger ui */
|
||||
#[derive(utoipa::ToSchema)]
|
||||
#[allow(dead_code)]
|
||||
struct DynamicForm(HashMap<String, String>);
|
||||
|
||||
/* Handler for calling RPC methods */
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/{rpc_method}",
|
||||
responses(
|
||||
(status = 201, description = "Call rpc method", body = serde_json::Value),
|
||||
(status = 201, description = "Call rpc method", body = serde_json::Value,
|
||||
content(("application/json"),("application/yaml"),("application/xml"),
|
||||
("application/x-www-form-urlencoded"))),
|
||||
(status = 401, description = "Unauthorized", body = serde_json::Value),
|
||||
(status = 403, description = "Forbidden", body = serde_json::Value),
|
||||
(status = 404, description = "Not Found", body = serde_json::Value),
|
||||
(status = 500, description = "Server Error", body = serde_json::Value)
|
||||
),
|
||||
request_body(content = serde_json::Value, content_type = "application/json",
|
||||
example = json!({}) ),
|
||||
request_body(description = "RPC params",
|
||||
content((newaddr = "application/json"),
|
||||
(newaddr = "application/yaml"),
|
||||
(DynamicForm = "application/x-www-form-urlencoded"),
|
||||
(newaddr = "application/xml"))),
|
||||
security(("api_key" = []))
|
||||
)]
|
||||
pub async fn call_rpc_method(
|
||||
@@ -109,7 +130,7 @@ pub async fn call_rpc_method(
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(String::from);
|
||||
|
||||
let bytes = match to_bytes(body.into_body(), usize::MAX).await {
|
||||
let request_bytes = match to_bytes(body.into_body(), usize::MAX).await {
|
||||
Ok(o) => o,
|
||||
Err(e) => {
|
||||
return Err(AppError::InternalServerError(RpcError {
|
||||
@@ -120,52 +141,207 @@ pub async fn call_rpc_method(
|
||||
}
|
||||
};
|
||||
|
||||
let mut rpc_params = match serde_json::from_slice(&bytes) {
|
||||
Ok(o) => o,
|
||||
Err(e1) => {
|
||||
/* it's not json but a form instead */
|
||||
let form_str = String::from_utf8(bytes.to_vec()).unwrap();
|
||||
let mut form_data = HashMap::new();
|
||||
for pair in form_str.split('&') {
|
||||
let mut kv = pair.split('=');
|
||||
if let (Some(key), Some(value)) = (kv.next(), kv.next()) {
|
||||
form_data.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
match serde_json::to_value(form_data) {
|
||||
Ok(o) => o,
|
||||
Err(e2) => {
|
||||
return Err(AppError::InternalServerError(RpcError {
|
||||
code: None,
|
||||
data: None,
|
||||
message: format!(
|
||||
"Could not parse json from form data: {}\
|
||||
Original serde_json error: {}",
|
||||
e2, e1
|
||||
),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut rpc_params = convert_request_to_json(&headers, &rpc_method, request_bytes)?;
|
||||
|
||||
filter_json(&mut rpc_params);
|
||||
|
||||
verify_rune(plugin.clone(), rune, &rpc_method, &rpc_params).await?;
|
||||
|
||||
match call_rpc(plugin, &rpc_method, rpc_params).await {
|
||||
Ok(result) => {
|
||||
let response_body = Json(result);
|
||||
let response = (StatusCode::CREATED, response_body).into_response();
|
||||
Ok(response)
|
||||
}
|
||||
let cln_result = match call_rpc(plugin, &rpc_method, rpc_params).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
if let Some(code) = err.code {
|
||||
if code == -32601 {
|
||||
return Err(AppError::NotFound(err));
|
||||
}
|
||||
}
|
||||
Err(AppError::InternalServerError(err))
|
||||
return Err(AppError::InternalServerError(err));
|
||||
}
|
||||
};
|
||||
|
||||
convert_json_to_response(headers, &rpc_method, cln_result)
|
||||
}
|
||||
|
||||
fn convert_request_to_json(
|
||||
headers: &axum::http::HeaderMap,
|
||||
rpc_method: &str,
|
||||
request_bytes: axum::body::Bytes,
|
||||
) -> Result<serde_json::Value, AppError> {
|
||||
let content_type = headers
|
||||
.get("content-type")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("application/json");
|
||||
|
||||
let format = match content_type {
|
||||
a if a.contains("*/*") => "json",
|
||||
a if a.contains("application/json") => "json",
|
||||
a if a.contains("application/yaml") => "yaml",
|
||||
a if a.contains("application/xml") => "xml",
|
||||
a if a.contains("application/x-www-form-urlencoded") => "form",
|
||||
_ => {
|
||||
return Err(AppError::NotAcceptable(RpcError {
|
||||
code: None,
|
||||
data: None,
|
||||
message: format!("Unsupported content-type header: {}", content_type),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
if request_bytes.is_empty() {
|
||||
return Ok(json!({}));
|
||||
}
|
||||
|
||||
match format {
|
||||
"yaml" => serde_yml::from_slice(&request_bytes).map_err(|e| {
|
||||
AppError::InternalServerError(RpcError {
|
||||
code: None,
|
||||
data: None,
|
||||
message: format!(
|
||||
"Could not parse `{}` as YAML request: {}",
|
||||
String::from_utf8_lossy(&request_bytes),
|
||||
e
|
||||
),
|
||||
})
|
||||
}),
|
||||
"xml" => {
|
||||
let req_str = std::str::from_utf8(&request_bytes).map_err(|e| {
|
||||
AppError::InternalServerError(RpcError {
|
||||
code: None,
|
||||
data: None,
|
||||
message: format!(
|
||||
"Could not read `{}` as valid utf8: {}",
|
||||
String::from_utf8_lossy(&request_bytes),
|
||||
e
|
||||
),
|
||||
})
|
||||
})?;
|
||||
let json_with_root = roxmltree_to_serde::xml_str_to_json(
|
||||
req_str,
|
||||
&roxmltree_to_serde::Config::new_with_defaults(),
|
||||
)
|
||||
.map_err(|e| {
|
||||
AppError::InternalServerError(RpcError {
|
||||
code: None,
|
||||
data: None,
|
||||
message: format!(
|
||||
"Could not parse `{}` as XML request: {}",
|
||||
String::from_utf8_lossy(&request_bytes),
|
||||
e
|
||||
),
|
||||
})
|
||||
})?;
|
||||
let json_without_root = json_with_root.get(rpc_method).ok_or_else(|| {
|
||||
AppError::InternalServerError(RpcError {
|
||||
code: None,
|
||||
data: None,
|
||||
message: format!("Use rpc method name as root element: `{}`", rpc_method),
|
||||
})
|
||||
})?;
|
||||
Ok(json!(json_without_root))
|
||||
}
|
||||
"form" => {
|
||||
let form_map: HashMap<String, serde_json::Value> = serde_qs::from_bytes(&request_bytes)
|
||||
.map_err(|e| {
|
||||
AppError::InternalServerError(RpcError {
|
||||
code: None,
|
||||
data: None,
|
||||
message: format!(
|
||||
"Could not parse `{}` FORM-URLENCODED request: {}",
|
||||
String::from_utf8_lossy(&request_bytes),
|
||||
e
|
||||
),
|
||||
})
|
||||
})?;
|
||||
Ok(json!(form_map))
|
||||
}
|
||||
_ => serde_json::from_slice(&request_bytes).map_err(|e| {
|
||||
AppError::InternalServerError(RpcError {
|
||||
code: None,
|
||||
data: None,
|
||||
message: format!(
|
||||
"Could not parse `{}` JSON request: {}",
|
||||
String::from_utf8_lossy(&request_bytes),
|
||||
e
|
||||
),
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_json_to_response(
|
||||
headers: axum::http::HeaderMap,
|
||||
rpc_method: &str,
|
||||
cln_result: serde_json::Value,
|
||||
) -> Result<Response, AppError> {
|
||||
let accept = headers
|
||||
.get("accept")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("application/json");
|
||||
|
||||
let format = match accept {
|
||||
a if a.contains("*/*") => "json",
|
||||
a if a.contains("application/json") => "json",
|
||||
a if a.contains("application/yaml") => "yaml",
|
||||
a if a.contains("application/xml") => "xml",
|
||||
a if a.contains("application/x-www-form-urlencoded") => "form",
|
||||
_ => {
|
||||
return Err(AppError::NotAcceptable(RpcError {
|
||||
code: None,
|
||||
data: None,
|
||||
message: format!("Unsupported accept header: {}", accept),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
match format {
|
||||
"yaml" => match serde_yml::to_string(&cln_result) {
|
||||
Ok(yaml) => Ok((
|
||||
StatusCode::CREATED,
|
||||
[("Content-Type", "application/yaml")],
|
||||
yaml,
|
||||
)
|
||||
.into_response()),
|
||||
Err(e) => Err(AppError::InternalServerError(RpcError {
|
||||
code: None,
|
||||
data: None,
|
||||
message: format!("Could not serialize to YAML: {}", e),
|
||||
})),
|
||||
},
|
||||
"xml" => match quick_xml::se::to_string_with_root(rpc_method, &cln_result) {
|
||||
Ok(xml) => Ok((
|
||||
StatusCode::CREATED,
|
||||
[("Content-Type", "application/xml")],
|
||||
xml,
|
||||
)
|
||||
.into_response()),
|
||||
Err(e) => Err(AppError::InternalServerError(RpcError {
|
||||
code: None,
|
||||
data: None,
|
||||
message: format!("Could not serialize to XML: {}", e),
|
||||
})),
|
||||
},
|
||||
"form" => match serde_qs::to_string(&cln_result) {
|
||||
Ok(form) => Ok((
|
||||
StatusCode::CREATED,
|
||||
[("Content-Type", "application/x-www-form-urlencoded")],
|
||||
form,
|
||||
)
|
||||
.into_response()),
|
||||
Err(e) => Err(AppError::InternalServerError(RpcError {
|
||||
code: None,
|
||||
data: None,
|
||||
message: format!("Could not serialize to FORM-URLENCODED: {}", e),
|
||||
})),
|
||||
},
|
||||
_ => {
|
||||
let response_body = Json(cln_result);
|
||||
let response = (
|
||||
StatusCode::CREATED,
|
||||
[("Content-Type", "application/json")],
|
||||
response_body,
|
||||
)
|
||||
.into_response();
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user