#[cfg(debug_assertions)] use log::info; #[cfg(debug_assertions)] use serde::Deserialize; use tide::{Request, Response}; mod database; mod indieauth; mod micropub; use crate::indieauth::check_auth; use crate::micropub::{get_handler,post_handler}; #[derive(Clone)] pub struct ApplicationState where StorageBackend: database::Storage + Send + Sync + 'static { token_endpoint: surf::Url, media_endpoint: Option, http_client: surf::Client, storage: StorageBackend } type App = tide::Server>; static INDEX_PAGE: &[u8] = include_bytes!("./index.html"); #[cfg(debug_assertions)] #[derive(Deserialize)] struct Mf2JsonQuery { url: String, limit: usize, user: Option, after: Option } fn equip_app(mut app: App) -> App where Storage: database::Storage + Send + Sync + Clone { app.at("/").get(|_: Request<_>| async move { Ok(Response::builder(200).body(INDEX_PAGE).content_type("text/html").build()) }); app.at("/micropub").with(check_auth).get(get_handler).post(post_handler); #[cfg(debug_assertions)] info!("Outfitting app with the debug function"); #[cfg(debug_assertions)] app.at("/mf2-json").get(|req: Request>| async move { info!("DEBUG FUNCTION: Reading MF2-JSON"); let backend = &req.state().storage; let query = req.query::()?; match backend.read_feed_with_limit(&query.url, &query.after, query.limit, &query.user).await { Ok(result) => match result { Some(post) => Ok(Response::builder(200).body(post).build()), None => Ok(Response::builder(404).build()) }, Err(err) => match err.kind() { database::ErrorKind::PermissionDenied => { if query.user.is_some() { Ok(Response::builder(403).build()) } else { Ok(Response::builder(401).build()) } } _ => Ok(Response::builder(500).body(serde_json::json!({"error": "database_error", "error_description": format!("{}", err)})).build()) } } }); app } pub async fn get_app_with_redis(token_endpoint: surf::Url, redis_uri: String, media_endpoint: Option) -> App { let app = tide::with_state(ApplicationState { token_endpoint, media_endpoint, storage: database::RedisStorage::new(redis_uri).await.unwrap(), http_client: surf::Client::new(), }); equip_app(app) } #[cfg(test)] pub async fn get_app_with_memory_for_testing(token_endpoint: surf::Url) -> (database::MemoryStorage, App) { let database = database::MemoryStorage::new(); let app = tide::with_state(ApplicationState { token_endpoint, media_endpoint: None, storage: database.clone(), http_client: surf::Client::new(), }); return (database, equip_app(app)) } #[cfg(test)] pub async fn get_app_with_test_redis(token_endpoint: surf::Url) -> (tempdir::TempDir, std::process::Child, database::RedisStorage, App) { let (tempdir, child, uri) = crate::database::get_redis_instance().await; let backend = database::RedisStorage::new(uri).await.unwrap(); let app = tide::with_state(ApplicationState { token_endpoint, media_endpoint: None, storage: backend.clone(), http_client: surf::Client::new(), }); return (tempdir, child, backend, equip_app(app)) } #[cfg(test)] #[allow(unused_variables,unused_imports)] mod tests { use super::*; use serde_json::json; use tide_testing::TideTestingExt; use crate::database::Storage; use mockito::mock; // Helpers async fn create_app() -> (database::RedisStorage, App, tempdir::TempDir, std::process::Child) { //get_app_with_memory_for_testing(surf::Url::parse(&*mockito::server_url()).unwrap()).await let (t, c, b, a) = get_app_with_test_redis(surf::Url::parse(&*mockito::server_url()).unwrap()).await; (b, a, t, c) } async fn post_json(app: &App, json: serde_json::Value) -> surf::Response { let request = app.post("/micropub") .header("Authorization", "Bearer test") .header("Content-Type", "application/json") .body(json); return request.send().await.unwrap(); } #[async_std::test] async fn test_no_posting_to_others_websites() { let _m = mock("GET", "/") .with_status(200) .with_header("Content-Type", "application/json") .with_body(r#"{"me": "https://fireburn.ru", "client_id": "https://quill.p3k.io/", "scope": "create update media"}"#) .create(); let (db, app, tempdir, mut child) = create_app().await; let response = post_json(&app, json!({ "type": ["h-entry"], "properties": { "content": ["Fake news about Aaron Parecki!"], "uid": ["https://aaronparecki.com/posts/fake-news"] } })).await; assert_eq!(response.status(), 403); let response = post_json(&app, json!({ "type": ["h-entry"], "properties": { "content": ["More fake news about Aaron Parecki!"], "url": ["https://aaronparecki.com/posts/more-fake-news"] } })).await; assert_eq!(response.status(), 403); let response = post_json(&app, json!({ "type": ["h-entry"], "properties": { "content": ["Sneaky advertisement designed to creep into someone else's feed! Buy whatever I'm promoting!"], "channel": ["https://aaronparecki.com/feeds/main"] } })).await; assert_eq!(response.status(), 403); child.kill().expect("Couldn't kill Redis"); } #[async_std::test] async fn test_successful_authorization() { let _m = mock("GET", "/") .with_status(200) .with_header("Content-Type", "application/json") .with_body(r#"{"me": "https://fireburn.ru", "client_id": "https://quill.p3k.io/", "scope": "create update media"}"#) .create(); let (db, app, tempdir, mut child) = create_app().await; let response: serde_json::Value = app.get("/micropub?q=config") .header("Authorization", "test") .recv_json().await.unwrap(); assert!(!response["q"].as_array().unwrap().is_empty()); child.kill().expect("Couldn't kill Redis"); } #[async_std::test] async fn test_unsuccessful_authorization() { let _m = mock("GET", "/") .with_status(400) .with_header("Content-Type", "application/json") .with_body(r#"{"error":"unauthorized","error_description":"A valid access token is required."}"#) .create(); let (db, app, tempdir, mut child) = create_app().await; let response: surf::Response = app.get("/micropub?q=config") .header("Authorization", "test") .send().await.unwrap(); assert_eq!(response.status(), 401); child.kill().expect("Couldn't kill Redis"); } #[async_std::test] async fn test_no_auth_header() { let (db, app, tempdir, mut child) = create_app().await; let request: surf::RequestBuilder = app.get("/micropub?q=config"); let response: surf::Response = request.send().await.unwrap(); assert_eq!(response.status(), 401); child.kill().expect("Couldn't kill Redis"); } #[async_std::test] async fn test_create_post_form_encoded() { let _m = mock("GET", "/") .with_status(200) .with_header("Content-Type", "application/json") .with_body(r#"{"me": "https://fireburn.ru", "client_id": "https://quill.p3k.io/", "scope": "create update media"}"#) .create(); let (storage, app, tempdir, mut child) = create_app().await; let request: surf::RequestBuilder = app.post("/micropub") .header("Authorization", "Bearer test") .header("Content-Type", "application/x-www-form-urlencoded") .body("h=entry&content=something%20interesting&category[]=test&category[]=stuff"); let mut response: surf::Response = request.send().await.unwrap(); println!("{:#}", response.body_json::().await.unwrap()); assert!(response.status() == 201 || response.status() == 202); let uid = response.header("Location").unwrap().last().to_string(); // Assume the post is in the database at this point. let post = storage.get_post(&uid).await.unwrap().unwrap(); assert_eq!(post["properties"]["content"][0]["html"].as_str().unwrap().trim(), "

something interesting

"); child.kill().expect("Couldn't kill Redis"); } #[async_std::test] async fn test_create_post_json() { let _m = mock("GET", "/") .with_status(200) .with_header("Content-Type", "application/json") .with_body(r#"{"me": "https://fireburn.ru", "client_id": "https://quill.p3k.io/", "scope": "create update media"}"#) .create(); let (storage, app, tempdir, mut child) = create_app().await; let mut response = post_json(&app, json!({ "type": ["h-entry"], "properties": { "content": ["This is content!"] } })).await; println!("{:#}", response.body_json::().await.unwrap()); assert!(response.status() == 201 || response.status() == 202); let uid = response.header("Location").unwrap().last().to_string(); // Assume the post is in the database at this point. let post = storage.get_post(&uid).await.unwrap().unwrap(); assert_eq!(post["properties"]["content"][0]["html"].as_str().unwrap().trim(), "

This is content!

"); let feed = storage.get_post("https://fireburn.ru/feeds/main").await.unwrap().unwrap(); assert_eq!(feed["children"].as_array().unwrap().len(), 1); assert_eq!(feed["children"][0].as_str().unwrap(), uid); let first_uid = uid; // Test creation of a second post let mut response = post_json(&app, json!({ "type": ["h-entry"], "properties": { "content": ["#moar content for you!"] } })).await; println!("{:#}", response.body_json::().await.unwrap()); assert!(response.status() == 201 || response.status() == 202); let uid = response.header("Location").unwrap().last().to_string(); // Assume the post is in the database at this point. //println!("Keys in database: {:?}", storage.mapping.read().await.keys()); let new_feed = storage.get_post("https://fireburn.ru/feeds/main").await.unwrap().unwrap(); println!("{}", new_feed["children"]); assert_eq!(new_feed["children"].as_array().unwrap().len(), 2); assert_eq!(new_feed["children"][0].as_str().unwrap(), uid); assert_eq!(new_feed["children"][1].as_str().unwrap(), first_uid); child.kill().expect("Couldn't kill Redis"); } }