Files
notepush/src/api_request_handler.rs
William Casarin 63c5f7e723 fix clippy issues
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-11 10:48:54 -08:00

547 lines
18 KiB
Rust

use crate::nip98_auth;
use crate::notification_manager::UserNotificationSettings;
use crate::relay_connection::RelayConnection;
use http_body_util::Full;
use hyper::body::Buf;
use hyper::body::Bytes;
use hyper::body::Incoming;
use hyper::{Request, Response, StatusCode};
use http_body_util::BodyExt;
use serde_json::from_value;
use crate::notification_manager::NotificationManager;
use hyper::Method;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::sync::Arc;
use thiserror::Error;
pub struct APIHandler {
notification_manager: Arc<NotificationManager>,
base_url: String,
}
impl APIHandler {
pub fn new(notification_manager: Arc<NotificationManager>, base_url: String) -> Self {
APIHandler {
notification_manager,
base_url,
}
}
// MARK: - HTTP handling
pub async fn handle_http_request(
&self,
req: Request<Incoming>,
) -> Result<Response<Full<Bytes>>, hyper::http::Error> {
// Check if the request is a websocket upgrade request.
if hyper_tungstenite::is_upgrade_request(&req) {
return match self.handle_websocket_upgrade(req).await {
Ok(response) => Ok(response),
Err(err) => {
log::error!("Error handling websocket upgrade request: {}", err);
Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(http_body_util::Full::new(Bytes::from(
"Internal server error",
)))?)
}
};
}
// If not, handle the request as a normal API request.
let final_api_response: APIResponse = match self.try_to_handle_http_request(req).await {
Ok(api_response) => APIResponse {
status: api_response.status,
body: api_response.body,
},
Err(err) => {
// Detect if error is a APIError::AuthenticationError and return a 401 status code
if let Some(api_error) = err.downcast_ref::<APIError>() {
match api_error {
APIError::AuthenticationError(message) => APIResponse {
status: StatusCode::UNAUTHORIZED,
body: json!({ "error": "Unauthorized", "message": message }),
},
}
} else {
// Otherwise, return a 500 status code
let random_case_uuid = uuid::Uuid::new_v4();
log::error!(
"Error handling request: {} (Case ID: {})",
err,
random_case_uuid
);
APIResponse {
status: StatusCode::INTERNAL_SERVER_ERROR,
body: json!({ "error": "Internal server error", "message": format!("Case ID: {}", random_case_uuid) }),
}
}
}
};
Response::builder()
.header("Content-Type", "application/json")
.header("Access-Control-Allow-Origin", "*")
.status(final_api_response.status)
.body(http_body_util::Full::new(Bytes::from(
final_api_response.body.to_string(),
)))
}
async fn handle_websocket_upgrade(
&self,
mut req: Request<Incoming>,
) -> Result<Response<Full<Bytes>>, Box<dyn std::error::Error>> {
let (response, websocket) = hyper_tungstenite::upgrade(&mut req, None)?;
log::info!("New websocket connection.");
let new_notification_manager = self.notification_manager.clone();
tokio::spawn(async move {
match RelayConnection::run(websocket, new_notification_manager).await {
Ok(_) => {}
Err(e) => {
log::error!("Error with websocket connection: {:?}", e);
}
}
});
Ok(response)
}
async fn try_to_handle_http_request(
&self,
mut req: Request<Incoming>,
) -> Result<APIResponse, Box<dyn std::error::Error>> {
let parsed_request = self.parse_http_request(&mut req).await?;
let api_response: APIResponse = self.handle_parsed_http_request(&parsed_request).await?;
log::info!(
"[{}] {} (Authorized pubkey: {}): {}",
req.method(),
req.uri(),
parsed_request.authorized_pubkey,
api_response.status
);
Ok(api_response)
}
async fn parse_http_request(
&self,
req: &mut Request<Incoming>,
) -> Result<ParsedRequest, Box<dyn std::error::Error>> {
// 1. Read the request body
let body_buffer = req.body_mut().collect().await?.aggregate();
let body_bytes = body_buffer.chunk();
let body_bytes = if body_bytes.is_empty() {
None
} else {
Some(body_bytes)
};
// 2. NIP-98 authentication
let authorized_pubkey = match self.authenticate(req, body_bytes).await? {
Ok(pubkey) => pubkey,
Err(auth_error) => {
return Err(Box::new(APIError::AuthenticationError(auth_error)));
}
};
// 3. Parse the request
Ok(ParsedRequest {
uri: req.uri().path().to_string(),
method: req.method().clone(),
body_bytes: body_bytes.map(|b| b.to_vec()),
authorized_pubkey,
})
}
// MARK: - Router
async fn handle_parsed_http_request(
&self,
parsed_request: &ParsedRequest,
) -> Result<APIResponse, Box<dyn std::error::Error>> {
if let Some(url_params) = route_match(
&Method::PUT,
"/user-info/:pubkey/:deviceToken",
parsed_request,
) {
return self.handle_user_info(parsed_request, &url_params).await;
}
if let Some(url_params) = route_match(
&Method::DELETE,
"/user-info/:pubkey/:deviceToken",
parsed_request,
) {
return self
.handle_user_info_remove(parsed_request, &url_params)
.await;
}
if let Some(url_params) = route_match(
&Method::GET,
"/user-info/:pubkey/:deviceToken/preferences",
parsed_request,
) {
return self.get_user_settings(parsed_request, &url_params).await;
}
if let Some(url_params) = route_match(
&Method::PUT,
"/user-info/:pubkey/:deviceToken/preferences",
parsed_request,
) {
return self.set_user_settings(parsed_request, &url_params).await;
}
Ok(APIResponse {
status: StatusCode::NOT_FOUND,
body: json!({ "error": "Not found" }),
})
}
// MARK: - Authentication
async fn authenticate(
&self,
req: &Request<Incoming>,
body_bytes: Option<&[u8]>,
) -> Result<Result<nostr::PublicKey, String>, Box<dyn std::error::Error>> {
let auth_header = match req.headers().get("Authorization") {
Some(header) => header,
None => return Ok(Err("Authorization header not found".to_string())),
};
Ok(nip98_auth::nip98_verify_auth_header(
auth_header.to_str()?.to_string(),
&format!("{}{}", self.base_url, req.uri().path()),
req.method().as_str(),
body_bytes,
)
.await)
}
// MARK: - Endpoint handlers
async fn handle_user_info(
&self,
req: &ParsedRequest,
url_params: &HashMap<&str, String>,
) -> Result<APIResponse, Box<dyn std::error::Error>> {
// Early return if `deviceToken` is missing
let device_token = match url_params.get("deviceToken") {
Some(token) => token,
None => {
return Ok(APIResponse {
status: StatusCode::BAD_REQUEST,
body: json!({ "error": "deviceToken is required on the URL" }),
})
}
};
// Early return if `pubkey` is missing
let pubkey = match url_params.get("pubkey") {
Some(key) => key,
None => {
return Ok(APIResponse {
status: StatusCode::BAD_REQUEST,
body: json!({ "error": "pubkey is required on the URL" }),
})
}
};
// Validate the `pubkey` and prepare it for use
let pubkey = match nostr::PublicKey::from_hex(pubkey) {
Ok(key) => key,
Err(_) => {
return Ok(APIResponse {
status: StatusCode::BAD_REQUEST,
body: json!({ "error": "Invalid pubkey" }),
})
}
};
// Early return if `pubkey` does not match `req.authorized_pubkey`
if pubkey != req.authorized_pubkey {
return Ok(APIResponse {
status: StatusCode::FORBIDDEN,
body: json!({ "error": "Forbidden" }),
});
}
// Proceed with the main logic after passing all checks
self.notification_manager
.save_user_device_info_if_not_present(pubkey, device_token)
.await?;
Ok(APIResponse {
status: StatusCode::OK,
body: json!({ "message": "User info saved successfully" }),
})
}
async fn handle_user_info_remove(
&self,
req: &ParsedRequest,
url_params: &HashMap<&str, String>,
) -> Result<APIResponse, Box<dyn std::error::Error>> {
// Early return if `deviceToken` is missing
let device_token = match url_params.get("deviceToken") {
Some(token) => token,
None => {
return Ok(APIResponse {
status: StatusCode::BAD_REQUEST,
body: json!({ "error": "deviceToken is required on the URL" }),
})
}
};
// Early return if `pubkey` is missing
let pubkey = match url_params.get("pubkey") {
Some(key) => key,
None => {
return Ok(APIResponse {
status: StatusCode::BAD_REQUEST,
body: json!({ "error": "pubkey is required on the URL" }),
})
}
};
// Validate the `pubkey` and prepare it for use
let pubkey = match nostr::PublicKey::from_hex(pubkey) {
Ok(key) => key,
Err(_) => {
return Ok(APIResponse {
status: StatusCode::BAD_REQUEST,
body: json!({ "error": "Invalid pubkey" }),
})
}
};
// Early return if `pubkey` does not match `req.authorized_pubkey`
if pubkey != req.authorized_pubkey {
return Ok(APIResponse {
status: StatusCode::FORBIDDEN,
body: json!({ "error": "Forbidden" }),
});
}
// Proceed with the main logic after passing all checks
self.notification_manager
.remove_user_device_info(pubkey, device_token)
.await?;
Ok(APIResponse {
status: StatusCode::OK,
body: json!({ "message": "User info removed successfully" }),
})
}
async fn set_user_settings(
&self,
req: &ParsedRequest,
url_params: &HashMap<&str, String>,
) -> Result<APIResponse, Box<dyn std::error::Error>> {
// Early return if `deviceToken` is missing
let device_token = match url_params.get("deviceToken") {
Some(token) => token,
None => {
return Ok(APIResponse {
status: StatusCode::BAD_REQUEST,
body: json!({ "error": "deviceToken is required on the URL" }),
})
}
};
// Early return if `pubkey` is missing
let pubkey = match url_params.get("pubkey") {
Some(key) => key,
None => {
return Ok(APIResponse {
status: StatusCode::BAD_REQUEST,
body: json!({ "error": "pubkey is required on the URL" }),
})
}
};
// Validate the `pubkey` and prepare it for use
let pubkey = match nostr::PublicKey::from_hex(pubkey) {
Ok(key) => key,
Err(_) => {
return Ok(APIResponse {
status: StatusCode::BAD_REQUEST,
body: json!({ "error": "Invalid pubkey" }),
})
}
};
// Early return if `pubkey` does not match `req.authorized_pubkey`
if pubkey != req.authorized_pubkey {
return Ok(APIResponse {
status: StatusCode::FORBIDDEN,
body: json!({ "error": "Forbidden" }),
});
}
// Proceed with the main logic after passing all checks
let body = req.body_json()?;
let settings: UserNotificationSettings = match from_value(body.clone()) {
Ok(settings) => settings,
Err(_) => {
return Ok(APIResponse {
status: StatusCode::BAD_REQUEST,
body: json!({ "error": "Invalid settings" }),
});
}
};
self.notification_manager
.save_user_notification_settings(
&req.authorized_pubkey,
device_token.to_string(),
settings,
)
.await?;
Ok(APIResponse {
status: StatusCode::OK,
body: json!({ "message": "User settings saved successfully" }),
})
}
async fn get_user_settings(
&self,
req: &ParsedRequest,
url_params: &HashMap<&str, String>,
) -> Result<APIResponse, Box<dyn std::error::Error>> {
// Early return if `deviceToken` is missing
let device_token = match url_params.get("deviceToken") {
Some(token) => token,
None => {
return Ok(APIResponse {
status: StatusCode::BAD_REQUEST,
body: json!({ "error": "deviceToken is required on the URL" }),
})
}
};
// Early return if `pubkey` is missing
let pubkey = match url_params.get("pubkey") {
Some(key) => key,
None => {
return Ok(APIResponse {
status: StatusCode::BAD_REQUEST,
body: json!({ "error": "pubkey is required on the URL" }),
})
}
};
// Validate the `pubkey` and prepare it for use
let pubkey = match nostr::PublicKey::from_hex(pubkey) {
Ok(key) => key,
Err(_) => {
return Ok(APIResponse {
status: StatusCode::BAD_REQUEST,
body: json!({ "error": "Invalid pubkey" }),
})
}
};
// Early return if `pubkey` does not match `req.authorized_pubkey`
if pubkey != req.authorized_pubkey {
return Ok(APIResponse {
status: StatusCode::FORBIDDEN,
body: json!({ "error": "Forbidden" }),
});
}
// Proceed with the main logic after passing all checks
let settings = self
.notification_manager
.get_user_notification_settings(&req.authorized_pubkey, device_token.to_string())
.await?;
Ok(APIResponse {
status: StatusCode::OK,
body: json!(settings),
})
}
}
// MARK: - Extensions
impl Clone for APIHandler {
fn clone(&self) -> Self {
APIHandler {
notification_manager: self.notification_manager.clone(),
base_url: self.base_url.clone(),
}
}
}
// MARK: - Helper types
// Define enum error types including authentication error
#[derive(Debug, Error)]
enum APIError {
#[error("Authentication error: {0}")]
AuthenticationError(String),
}
struct ParsedRequest {
uri: String,
method: Method,
body_bytes: Option<Vec<u8>>,
authorized_pubkey: nostr::PublicKey,
}
impl ParsedRequest {
fn body_json(&self) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
if let Some(body_bytes) = &self.body_bytes {
Ok(serde_json::from_slice(body_bytes)?)
} else {
Ok(json!({}))
}
}
}
struct APIResponse {
status: StatusCode,
body: Value,
}
// MARK: - Helper functions
/// Matches the request to a specified route, returning a hashmap of the route parameters
/// e.g. GET /user/:id/info route against request GET /user/123/info matches to { "id": "123" }
fn route_match<'a>(
method: &Method,
path: &'a str,
req: &ParsedRequest,
) -> Option<HashMap<&'a str, String>> {
if method != req.method {
return None;
}
let mut params = HashMap::new();
let path_segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let req_segments: Vec<&str> = req.uri.split('/').filter(|s| !s.is_empty()).collect();
if path_segments.len() != req_segments.len() {
return None;
}
for (i, segment) in path_segments.iter().enumerate() {
if let Some(key) = segment.strip_prefix(':') {
let value = req_segments[i].to_string();
params.insert(key, value);
} else if segment != &req_segments[i] {
return None;
}
}
Some(params)
}