diff --git a/.woodpecker.yml b/.woodpecker.yml index 220540f..81a0c95 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -11,13 +11,13 @@ steps: - rustup component add rustfmt - cargo fmt -- --check - frontend_wasm_build: + check_config_defaults_updated: image: *rust_image environment: CARGO_HOME: .cargo_home commands: - - "rustup target add wasm32-unknown-unknown" - - "cargo check --target wasm32-unknown-unknown --features csr,hydrate --no-default-features" + - cargo run -- --print-config > config/defaults_current.toml + - diff config/defaults.toml config/defaults_current.toml check_diesel_schema: image: willsquire/diesel-cli @@ -29,6 +29,14 @@ steps: - diesel print-schema --config-file=diesel.toml > tmp.schema - diff tmp.schema src/backend/database/schema.rs + frontend_wasm_build: + image: *rust_image + environment: + CARGO_HOME: .cargo_home + commands: + - "rustup target add wasm32-unknown-unknown" + - "cargo check --target wasm32-unknown-unknown --features csr,hydrate --no-default-features" + cargo_clippy: image: *rust_image environment: diff --git a/Cargo.lock b/Cargo.lock index faf8405..1d6d8d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -348,6 +348,9 @@ name = "bitflags" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +dependencies = [ + "serde", +] [[package]] name = "block-buffer" @@ -412,7 +415,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c878c71c2821aa2058722038a59a67583a4240524687c6028571c9b395ded61f" dependencies = [ - "darling", + "darling 0.14.4", "proc-macro2", "quote", "syn 1.0.109", @@ -542,6 +545,26 @@ dependencies = [ "toml 0.5.11", ] +[[package]] +name = "config" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" +dependencies = [ + "async-trait", + "convert_case 0.6.0", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml 0.8.8", + "yaml-rust", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -562,6 +585,26 @@ dependencies = [ "web-sys", ] +[[package]] +name = "const-random" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaf16c9c2c612020bcfd042e170f6e32de9b9d75adb5277cdbbd2e2c8c8299a" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "const_format" version = "0.2.32" @@ -702,6 +745,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -712,14 +761,38 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + [[package]] name = "darling" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", ] [[package]] @@ -736,13 +809,24 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core 0.13.4", + "quote", + "syn 1.0.109", +] + [[package]] name = "darling_macro" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ - "darling_core", + "darling_core 0.14.4", "quote", "syn 1.0.109", ] @@ -793,7 +877,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" dependencies = [ - "darling", + "darling 0.14.4", "proc-macro2", "quote", "syn 1.0.109", @@ -905,6 +989,38 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "doku" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d018fadaf95088d2c12b66fe5b9d7c04a027b996c42a7b403b83fbd7a1c31531" +dependencies = [ + "doku-derive", + "serde", + "serde_json", +] + +[[package]] +name = "doku-derive" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74073dd10495ce912909655131925b0459d49363751b93676148d843097fe825" +dependencies = [ + "darling 0.13.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "downcast-rs" version = "1.2.0" @@ -1491,12 +1607,14 @@ dependencies = [ "axum-macros", "bcrypt", "chrono", + "config 0.14.0", "console_error_panic_hook", "console_log", "diesel", "diesel-derive-newtype", "diesel_migrations", "diffy", + "doku", "enum_delegate", "env_logger", "futures", @@ -1515,6 +1633,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "smart-default", "time", "tokio", "tower", @@ -1661,6 +1780,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "jsonwebtoken" version = "9.2.0" @@ -1731,7 +1861,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afcaa5db5b22b794b624e14ffe2aefae215b2d21c60a230ae2d06fe21ae5da64" dependencies = [ - "config", + "config 0.13.4", "regex", "serde", "thiserror", @@ -2299,6 +2429,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +dependencies = [ + "dlv-list", + "hashbrown 0.13.2", +] + [[package]] name = "overload" version = "0.1.1" @@ -2362,6 +2502,51 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f200d8d83c44a45b21764d1916299752ca035d15ecd46faca3e9a2a2bf6ad06" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcd6ab1236bbdb3a49027e920e693192ebfe8913f6d60e294de57463a493cfde" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a31940305ffc96863a735bef7c7994a00b325a7138fdbc5bda0f1a0476d3275" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "pest_meta" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ff62f5259e53b78d1af898941cdcdccfae7385cf7d793a6e55de5d05bb4b7d" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "pin-project" version = "1.1.3" @@ -2770,6 +2955,18 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.5", + "bitflags 2.4.1", + "serde", + "serde_derive", +] + [[package]] name = "rstml" version = "0.11.2" @@ -2784,6 +2981,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "rust-ini" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -3125,6 +3332,17 @@ version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +[[package]] +name = "smart-default" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "socket2" version = "0.4.10" @@ -3335,6 +3553,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -3424,7 +3651,19 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.21.0", ] [[package]] @@ -3449,6 +3688,19 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap 2.1.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -3572,6 +3824,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "unicase" version = "2.7.0" diff --git a/Cargo.toml b/Cargo.toml index 9f73dfe..6ef57b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,9 @@ ssr = [ csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] +[lints.clippy] +dbg_macro = "deny" + [dependencies] activitypub_federation = { version = "0.5.0-beta.6", features = [ "axum", @@ -69,6 +72,9 @@ time = "0.3.31" tower = "0.4.13" markdown-it = "0.6.0" web-sys = "0.3.67" +config = { version = "0.14.0", features = ["toml"] } +doku = "0.21.1" +smart-default = "0.7.1" [dev-dependencies] pretty_assertions = "1.4.0" diff --git a/config/config.toml b/config/config.toml new file mode 100644 index 0000000..b963440 --- /dev/null +++ b/config/config.toml @@ -0,0 +1,3 @@ +[setup] +admin_username = "ibis" +admin_password = "ibis" diff --git a/config/defaults.toml b/config/defaults.toml new file mode 100644 index 0000000..8faf3d6 --- /dev/null +++ b/config/defaults.toml @@ -0,0 +1,25 @@ +# Address where ibis should listen for incoming requests +bind = "127.0.0.1:8081" + +# Database connection url +database_url = "postgres://ibis:password@localhost:5432/ibis" + +# Whether users can create new accounts +registration_open = true + +# Details of the initial admin account +[setup] +admin_username = "admin" +admin_password = "hunter2" + +[federation] +# Domain name of the instance, mandatory for federation +domain = "example.com" + +# Comma separated list of instances which are allowed for federation. If set, federation +# with other domains is blocked +# Optional +allowlist = "good.com,friends.org" + +# Comma separated list of instances which are blocked for federation; optional +blocklist = "evil.com,bad.org" diff --git a/src/backend/api/article.rs b/src/backend/api/article.rs index 124069d..79d00bb 100644 --- a/src/backend/api/article.rs +++ b/src/backend/api/article.rs @@ -6,12 +6,12 @@ use crate::backend::error::MyResult; use crate::backend::federation::activities::create_article::CreateArticle; use crate::backend::federation::activities::submit_article_update; use crate::backend::utils::generate_article_version; -use crate::common::LocalUserView; use crate::common::{ApiConflict, ResolveObject}; use crate::common::{ArticleView, DbArticle, DbEdit}; use crate::common::{CreateArticleData, EditArticleData, EditVersion, ForkArticleData}; use crate::common::{DbInstance, SearchArticleData}; use crate::common::{GetArticleData, ListArticlesData}; +use crate::common::{LocalUserView, MAIN_PAGE_NAME}; use activitypub_federation::config::Data; use activitypub_federation::fetch::object_id::ObjectId; use anyhow::anyhow; @@ -91,6 +91,12 @@ pub(in crate::backend::api) async fn edit_article( if edit_form.summary.is_empty() { return Err(anyhow!("No summary given").into()); } + if original_article.article.local + && original_article.article.title == MAIN_PAGE_NAME + && !user.local_user.admin + { + return Err(anyhow!("Only admin can edit main page").into()); + } // ensure trailing newline for clean diffs if !edit_form.new_text.ends_with('\n') { edit_form.new_text.push('\n'); diff --git a/src/backend/api/user.rs b/src/backend/api/user.rs index 8c86416..4b53371 100644 --- a/src/backend/api/user.rs +++ b/src/backend/api/user.rs @@ -58,7 +58,9 @@ pub(in crate::backend::api) async fn register_user( jar: CookieJar, Form(form): Form, ) -> MyResult<(CookieJar, Json)> { - // TODO: make admin if its the first user account + if !data.config.registration_open { + return Err(anyhow!("Registration is closed").into()); + } let user = DbPerson::create_local(form.username, form.password, false, &data)?; let token = generate_login_token(&user.local_user, &data)?; let jar = jar.add(create_cookie(token, &data)); diff --git a/src/backend/database/instance.rs b/src/backend/database/instance.rs index b4ca59a..5b2a38d 100644 --- a/src/backend/database/instance.rs +++ b/src/backend/database/instance.rs @@ -64,12 +64,10 @@ impl DbInstance { pub fn read_local_view(conn: &Mutex) -> MyResult { let instance = DbInstance::read_local_instance(conn)?; let followers = DbInstance::read_followers(instance.id, conn)?; - let following = DbInstance::read_following(instance.id, conn)?; Ok(InstanceView { instance, followers, - following, }) } @@ -106,14 +104,4 @@ impl DbInstance { .select(person::all_columns) .get_results(conn.deref_mut())?) } - - pub fn read_following(id_: i32, conn: &Mutex) -> MyResult> { - use instance_follow::dsl::{follower_id, instance_id}; - let mut conn = conn.lock().unwrap(); - Ok(instance_follow::table - .inner_join(instance::table.on(instance_id.eq(instance::dsl::id))) - .filter(follower_id.eq(id_)) - .select(instance::all_columns) - .get_results(conn.deref_mut())?) - } } diff --git a/src/backend/database/mod.rs b/src/backend/database/mod.rs index 253cc8f..3ce9071 100644 --- a/src/backend/database/mod.rs +++ b/src/backend/database/mod.rs @@ -1,9 +1,11 @@ use diesel::PgConnection; use std::ops::Deref; use std::sync::{Arc, Mutex}; -pub type MyDataHandle = MyData; +// TODO: can remove this +pub type MyDataHandle = IbisData; use crate::backend::database::schema::jwt_secret; use crate::backend::error::MyResult; +use crate::config::IbisConfig; use diesel::{QueryDsl, RunQueryDsl}; use std::ops::DerefMut; @@ -16,11 +18,12 @@ pub mod user; pub mod version; #[derive(Clone)] -pub struct MyData { +pub struct IbisData { pub db_connection: Arc>, + pub config: IbisConfig, } -impl Deref for MyData { +impl Deref for IbisData { type Target = Arc>; fn deref(&self) -> &Self::Target { diff --git a/src/backend/database/user.rs b/src/backend/database/user.rs index 4702170..e7fa6bb 100644 --- a/src/backend/database/user.rs +++ b/src/backend/database/user.rs @@ -1,18 +1,19 @@ +use crate::backend::database::schema::{instance, instance_follow}; use crate::backend::database::schema::{local_user, person}; -use crate::backend::database::MyDataHandle; +use crate::backend::database::{IbisData, MyDataHandle}; use crate::backend::error::MyResult; -use crate::common::{DbLocalUser, DbPerson, LocalUserView}; +use crate::common::{DbInstance, DbLocalUser, DbPerson, LocalUserView}; use activitypub_federation::config::Data; use activitypub_federation::fetch::object_id::ObjectId; use activitypub_federation::http_signatures::generate_actor_keypair; use bcrypt::hash; use bcrypt::DEFAULT_COST; use chrono::{DateTime, Local, Utc}; -use diesel::ExpressionMethods; use diesel::QueryDsl; use diesel::{insert_into, AsChangeset, Insertable, PgConnection, RunQueryDsl}; +use diesel::{ExpressionMethods, JoinOnDsl}; use std::ops::DerefMut; -use std::sync::Mutex; +use std::sync::{Mutex, MutexGuard}; #[derive(Debug, Clone, Insertable, AsChangeset)] #[diesel(table_name = local_user, check_for_backend(diesel::pg::Pg))] @@ -54,12 +55,12 @@ impl DbPerson { username: String, password: String, admin: bool, - data: &Data, + data: &IbisData, ) -> MyResult { let mut conn = data.db_connection.lock().unwrap(); - let hostname = data.domain(); - let ap_id = ObjectId::parse(&format!("http://{hostname}/user/{username}"))?; - let inbox_url = format!("http://{hostname}/inbox"); + let domain = &data.config.federation.domain; + let ap_id = ObjectId::parse(&format!("http://{domain}/user/{username}"))?; + let inbox_url = format!("http://{domain}/inbox"); let keypair = generate_actor_keypair()?; let person_form = DbPersonForm { username, @@ -85,7 +86,11 @@ impl DbPerson { .values(local_user_form) .get_result::(conn.deref_mut())?; - Ok(LocalUserView { local_user, person }) + Ok(LocalUserView { + local_user, + person, + following: vec![], + }) } pub fn read_from_ap_id( @@ -103,19 +108,42 @@ impl DbPerson { data: &Data, ) -> MyResult { let mut conn = data.db_connection.lock().unwrap(); - Ok(person::table + let (person, local_user) = person::table .inner_join(local_user::table) .filter(person::dsl::local) .filter(person::dsl::username.eq(username)) - .get_result(conn.deref_mut())?) + .get_result::<(DbPerson, DbLocalUser)>(conn.deref_mut())?; + // TODO: handle this in single query + let following = Self::read_following(person.id, conn)?; + Ok(LocalUserView { + person, + local_user, + following, + }) } pub fn read_local_from_id(id: i32, data: &Data) -> MyResult { let mut conn = data.db_connection.lock().unwrap(); - Ok(person::table + let (person, local_user) = person::table .inner_join(local_user::table) .filter(person::dsl::local) .filter(person::dsl::id.eq(id)) - .get_result(conn.deref_mut())?) + .get_result::<(DbPerson, DbLocalUser)>(conn.deref_mut())?; + // TODO: handle this in single query + let following = Self::read_following(person.id, conn)?; + Ok(LocalUserView { + person, + local_user, + following, + }) + } + + fn read_following(id_: i32, mut conn: MutexGuard) -> MyResult> { + use instance_follow::dsl::{follower_id, instance_id}; + Ok(instance_follow::table + .inner_join(instance::table.on(instance_id.eq(instance::dsl::id))) + .filter(follower_id.eq(id_)) + .select(instance::all_columns) + .get_results(conn.deref_mut())?) } } diff --git a/src/backend/federation/activities/follow.rs b/src/backend/federation/activities/follow.rs index 456e434..11c36b8 100644 --- a/src/backend/federation/activities/follow.rs +++ b/src/backend/federation/activities/follow.rs @@ -5,6 +5,7 @@ use crate::backend::{ }; use crate::common::DbInstance; use crate::common::DbPerson; +use activitypub_federation::protocol::verification::verify_urls_match; use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, @@ -58,6 +59,7 @@ impl ActivityHandler for Follow { async fn receive(self, data: &Data) -> Result<(), Self::Error> { let actor = self.actor.dereference(data).await?; let local_instance = DbInstance::read_local_instance(&data.db_connection)?; + verify_urls_match(self.object.inner(), local_instance.ap_id.inner())?; DbInstance::follow(&actor, &local_instance, false, data)?; // send back an accept diff --git a/src/backend/federation/mod.rs b/src/backend/federation/mod.rs index b9b3fbd..3feeba9 100644 --- a/src/backend/federation/mod.rs +++ b/src/backend/federation/mod.rs @@ -1,8 +1,11 @@ use crate::backend::database::MyDataHandle; +use crate::config::IbisConfig; use activitypub_federation::activity_sending::SendActivityTask; -use activitypub_federation::config::Data; +use activitypub_federation::config::{Data, UrlVerifier}; +use activitypub_federation::error::Error as ActivityPubError; use activitypub_federation::protocol::context::WithContext; use activitypub_federation::traits::{ActivityHandler, Actor}; +use async_trait::async_trait; use log::warn; use serde::Serialize; use std::fmt::Debug; @@ -32,3 +35,31 @@ where } Ok(()) } + +#[derive(Clone)] +pub struct VerifyUrlData(pub IbisConfig); + +#[async_trait] +impl UrlVerifier for VerifyUrlData { + /// Check domain against allowlist and blocklist from config file. + async fn verify(&self, url: &Url) -> Result<(), ActivityPubError> { + let domain = url.domain().unwrap(); + if let Some(allowlist) = &self.0.federation.allowlist { + let allowlist = allowlist.split(',').collect::>(); + if !allowlist.contains(&domain) { + return Err(ActivityPubError::Other(format!( + "Domain {domain} is not allowed" + ))); + } + } + if let Some(blocklist) = &self.0.federation.blocklist { + let blocklist = blocklist.split(',').collect::>(); + if blocklist.contains(&domain) { + return Err(ActivityPubError::Other(format!( + "Domain {domain} is blocked" + ))); + } + } + Ok(()) + } +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 961c69a..bdd9481 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,10 +1,13 @@ use crate::backend::database::article::DbArticleForm; use crate::backend::database::instance::DbInstanceForm; -use crate::backend::database::MyData; +use crate::backend::database::IbisData; +use crate::backend::error::Error; use crate::backend::error::MyResult; use crate::backend::federation::routes::federation_routes; +use crate::backend::federation::VerifyUrlData; use crate::backend::utils::generate_activity_id; -use crate::common::{DbArticle, DbInstance}; +use crate::common::{DbArticle, DbInstance, DbPerson, MAIN_PAGE_NAME}; +use crate::config::IbisConfig; use crate::frontend::app::App; use activitypub_federation::config::{FederationConfig, FederationMiddleware}; use activitypub_federation::fetch::collection_id::CollectionId; @@ -24,7 +27,6 @@ use diesel_migrations::MigrationHarness; use leptos::*; use leptos_axum::{generate_route_list, LeptosRoutes}; use log::info; -use std::net::ToSocketAddrs; use std::sync::{Arc, Mutex}; use tower::Layer; use tower_http::cors::CorsLayer; @@ -40,62 +42,37 @@ const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); const FEDERATION_ROUTES_PREFIX: &str = "/federation_routes"; -pub async fn start(hostname: &str, database_url: &str) -> MyResult<()> { - let db_connection = Arc::new(Mutex::new(PgConnection::establish(database_url)?)); +pub async fn start(config: IbisConfig) -> MyResult<()> { + let db_connection = Arc::new(Mutex::new(PgConnection::establish(&config.database_url)?)); db_connection .lock() .unwrap() .run_pending_migrations(MIGRATIONS) .unwrap(); - let data = MyData { db_connection }; - let config = FederationConfig::builder() - .domain(hostname) + let data = IbisData { + db_connection, + config, + }; + let data = FederationConfig::builder() + .domain(data.config.federation.domain.clone()) + .url_verifier(Box::new(VerifyUrlData(data.config.clone()))) .app_data(data) .debug(true) .build() .await?; // Create local instance if it doesnt exist yet - // TODO: Move this into setup api call - if DbInstance::read_local_instance(&config.db_connection).is_err() { - let ap_id = ObjectId::parse(&format!("http://{hostname}"))?; - let articles_url = CollectionId::parse(&format!("http://{}/all_articles", hostname))?; - let inbox_url = format!("http://{}/inbox", hostname); - let keypair = generate_actor_keypair()?; - let form = DbInstanceForm { - ap_id, - description: Some("New Ibis instance".to_string()), - articles_url, - inbox_url, - public_key: keypair.public_key, - private_key: Some(keypair.private_key), - last_refreshed_at: Local::now().into(), - local: true, - }; - let instance = DbInstance::create(&form, &config.db_connection)?; - - // Create the main page which is shown by default - let form = DbArticleForm { - title: "Main_Page".to_string(), - text: "Hello world!".to_string(), - ap_id: ObjectId::parse(&format!("http://{hostname}/article/Main_Page"))?, - instance_id: instance.id, - local: true, - }; - DbArticle::create(&form, &config.db_connection)?; + if DbInstance::read_local_instance(&data.db_connection).is_err() { + setup(&data)?; } let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); let mut leptos_options = conf.leptos_options; - let addr = hostname - .to_socket_addrs()? - .next() - .expect("Failed to lookup domain name"); - leptos_options.site_addr = addr; + leptos_options.site_addr = data.config.bind; let routes = generate_route_list(App); - let config = config.clone(); + let config = data.clone(); let app = Router::new() .leptos_routes(&leptos_options, routes, || view! { }) .with_state(leptos_options) @@ -118,14 +95,51 @@ pub async fn start(hostname: &str, database_url: &str) -> MyResult<()> { let middleware = axum::middleware::from_fn(federation_routes_middleware); let app_with_middleware = middleware.layer(app); - info!("{addr}"); - Server::bind(&addr) + info!("Listening on {}", &data.config.bind); + Server::bind(&data.config.bind) .serve(app_with_middleware.into_make_service()) .await?; Ok(()) } +fn setup(data: &IbisData) -> Result<(), Error> { + let domain = &data.config.federation.domain; + let ap_id = ObjectId::parse(&format!("http://{domain}"))?; + let articles_url = CollectionId::parse(&format!("http://{domain}/all_articles"))?; + let inbox_url = format!("http://{domain}/inbox"); + let keypair = generate_actor_keypair()?; + let form = DbInstanceForm { + ap_id, + description: Some("New Ibis instance".to_string()), + articles_url, + inbox_url, + public_key: keypair.public_key, + private_key: Some(keypair.private_key), + last_refreshed_at: Local::now().into(), + local: true, + }; + let instance = DbInstance::create(&form, &data.db_connection)?; + + // Create the main page which is shown by default + let form = DbArticleForm { + title: MAIN_PAGE_NAME.to_string(), + text: "Hello world!".to_string(), + ap_id: ObjectId::parse(&format!("http://{domain}/article/{MAIN_PAGE_NAME}"))?, + instance_id: instance.id, + local: true, + }; + DbArticle::create(&form, &data.db_connection)?; + + DbPerson::create_local( + data.config.setup.admin_username.clone(), + data.config.setup.admin_password.clone(), + true, + data, + )?; + Ok(()) +} + /// Rewrite federation routes to use `FEDERATION_ROUTES_PREFIX`, to avoid conflicts /// with frontend routes. If a request is an Activitypub fetch as indicated by /// `Accept: application/activity+json` header, use the federation routes. Otherwise diff --git a/src/common/mod.rs b/src/common/mod.rs index aae0a0f..54c0ee6 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -13,6 +13,8 @@ use { diesel::{Identifiable, Queryable, Selectable}, }; +pub const MAIN_PAGE_NAME: &str = "Main_Page"; + /// Should be an enum Title/Id but fails due to https://github.com/nox/serde_urlencoded/issues/66 #[derive(Deserialize, Serialize, Clone)] pub struct GetArticleData { @@ -123,6 +125,7 @@ pub struct LoginUserData { pub struct LocalUserView { pub person: DbPerson, pub local_user: DbLocalUser, + pub following: Vec, } /// A user with account registered on local instance. @@ -241,7 +244,6 @@ pub struct DbInstance { pub struct InstanceView { pub instance: DbInstance, pub followers: Vec, - pub following: Vec, } #[test] diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..017ab2a --- /dev/null +++ b/src/config.rs @@ -0,0 +1,51 @@ +use doku::Document; +use serde::Deserialize; +use smart_default::SmartDefault; +use std::net::SocketAddr; + +#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)] +#[serde(default)] +pub struct IbisConfig { + /// Address where ibis should listen for incoming requests + #[default("127.0.0.1:8081".parse().unwrap())] + #[doku(as = "String", example = "127.0.0.1:8081")] + pub bind: SocketAddr, + /// Database connection url + #[default("postgres://ibis:password@localhost:5432/ibis")] + #[doku(example = "postgres://ibis:password@localhost:5432/ibis")] + pub database_url: String, + /// Whether users can create new accounts + #[default = true] + #[doku(example = "true")] + pub registration_open: bool, + /// Details of the initial admin account + pub setup: IbisConfigSetup, + pub federation: IbisConfigFederation, +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)] +#[serde(default)] +pub struct IbisConfigSetup { + #[doku(example = "admin")] + pub admin_username: String, + #[doku(example = "hunter2")] + pub admin_password: String, +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)] +#[serde(default)] +pub struct IbisConfigFederation { + /// Domain name of the instance, mandatory for federation + #[default("example.com")] + #[doku(example = "example.com")] + pub domain: String, + /// Comma separated list of instances which are allowed for federation. If set, federation + /// with other domains is blocked + #[default(None)] + #[doku(example = "good.com,friends.org")] + pub allowlist: Option, + /// Comma separated list of instances which are blocked for federation + #[default(None)] + #[doku(example = "evil.com,bad.org")] + pub blocklist: Option, +} diff --git a/src/frontend/components/nav.rs b/src/frontend/components/nav.rs index 32fbddf..e193e61 100644 --- a/src/frontend/components/nav.rs +++ b/src/frontend/components/nav.rs @@ -13,6 +13,7 @@ pub fn Nav() -> impl IntoView { .update_my_profile(); }); let (search_query, set_search_query) = create_signal(String::new()); + // TODO: hide register button if disabled in config view! {