add config file

This commit is contained in:
Felix Ableitner 2024-02-07 16:54:43 +01:00
parent dbd8e931a4
commit 334dc3826f
22 changed files with 557 additions and 116 deletions

View File

@ -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:

272
Cargo.lock generated
View File

@ -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"

View File

@ -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"

3
config/config.toml Normal file
View File

@ -0,0 +1,3 @@
[setup]
admin_username = "ibis"
admin_password = "ibis"

25
config/defaults.toml Normal file
View File

@ -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"

View File

@ -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');

View File

@ -58,7 +58,9 @@ pub(in crate::backend::api) async fn register_user(
jar: CookieJar,
Form(form): Form<RegisterUserData>,
) -> MyResult<(CookieJar, Json<LocalUserView>)> {
// 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));

View File

@ -64,12 +64,10 @@ impl DbInstance {
pub fn read_local_view(conn: &Mutex<PgConnection>) -> MyResult<InstanceView> {
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<PgConnection>) -> MyResult<Vec<Self>> {
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())?)
}
}

View File

@ -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<Mutex<PgConnection>>,
pub config: IbisConfig,
}
impl Deref for MyData {
impl Deref for IbisData {
type Target = Arc<Mutex<PgConnection>>;
fn deref(&self) -> &Self::Target {

View File

@ -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<MyDataHandle>,
data: &IbisData,
) -> MyResult<LocalUserView> {
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::<DbLocalUser>(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<MyDataHandle>,
) -> MyResult<LocalUserView> {
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<MyDataHandle>) -> MyResult<LocalUserView> {
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<PgConnection>) -> MyResult<Vec<DbInstance>> {
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())?)
}
}

View File

@ -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<Self::DataType>) -> 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

View File

@ -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::<Vec<_>>();
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::<Vec<_>>();
if blocklist.contains(&domain) {
return Err(ActivityPubError::Other(format!(
"Domain {domain} is blocked"
)));
}
}
Ok(())
}
}

View File

@ -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! { <App/> })
.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

View File

@ -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<DbInstance>,
}
/// A user with account registered on local instance.
@ -241,7 +244,6 @@ pub struct DbInstance {
pub struct InstanceView {
pub instance: DbInstance,
pub followers: Vec<DbPerson>,
pub following: Vec<DbInstance>,
}
#[test]

51
src/config.rs Normal file
View File

@ -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<String>,
/// Comma separated list of instances which are blocked for federation
#[default(None)]
#[doku(example = "evil.com,bad.org")]
pub blocklist: Option<String>,
}

View File

@ -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! {
<nav class="inner">
<li>

View File

@ -1,4 +1,4 @@
use crate::common::{ArticleView, GetArticleData};
use crate::common::{ArticleView, GetArticleData, MAIN_PAGE_NAME};
use crate::frontend::app::GlobalState;
use leptos::{create_resource, Resource};
@ -13,7 +13,7 @@ fn article_resource(
title: impl Fn() -> Option<String> + 'static,
) -> Resource<Option<String>, ArticleView> {
create_resource(title, move |title| async move {
let title = title.unwrap_or("Main_Page".to_string());
let title = title.unwrap_or(MAIN_PAGE_NAME.to_string());
GlobalState::api_client()
.get_article(GetArticleData {
title: Some(title),

View File

@ -1,4 +1,5 @@
#[cfg(feature = "ssr")]
pub mod backend;
pub mod common;
pub mod config;
pub mod frontend;

View File

@ -1,16 +1,29 @@
#[cfg(feature = "ssr")]
#[tokio::main]
pub async fn main() -> ibis_lib::backend::error::MyResult<()> {
use config::Config;
use ibis_lib::config::IbisConfig;
use log::LevelFilter;
if std::env::args().collect::<Vec<_>>().get(1) == Some(&"--print-config".to_string()) {
println!("{}", doku::to_toml::<IbisConfig>());
std::process::exit(0);
}
env_logger::builder()
.filter_level(LevelFilter::Warn)
.filter_module("activitypub_federation", LevelFilter::Info)
.filter_module("ibis", LevelFilter::Info)
.init();
let database_url = std::env::var("IBIS_DATABASE_URL")
.unwrap_or("postgres://ibis:password@localhost:5432/ibis".to_string());
let port = std::env::var("IBIS_BACKEND_PORT").unwrap_or("8081".to_string());
ibis_lib::backend::start(&format!("127.0.0.1:{port}"), &database_url).await?;
let config = Config::builder()
.add_source(config::File::with_name("config/config.toml"))
.add_source(config::Environment::with_prefix("IBIS"))
.build()
.unwrap();
let ibis_config: IbisConfig = config.try_deserialize().unwrap();
ibis_lib::backend::start(ibis_config).await?;
Ok(())
}

View File

@ -1,16 +1,14 @@
use ibis_lib::backend::start;
use ibis_lib::common::RegisterUserData;
use ibis_lib::config::{IbisConfig, IbisConfigFederation};
use ibis_lib::frontend::api::ApiClient;
use ibis_lib::frontend::error::MyResult;
use reqwest::ClientBuilder;
use std::env::current_dir;
use std::fs::create_dir_all;
use std::ops::Deref;
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Once;
use std::thread::{sleep, spawn};
use std::time::Duration;
@ -103,11 +101,21 @@ impl IbisInstance {
}
async fn start(db_path: String, port: i32, username: &str) -> Self {
let db_url = format!("postgresql://lemmy:password@/lemmy?host={db_path}");
let database_url = format!("postgresql://ibis:password@/ibis?host={db_path}");
let hostname = format!("localhost:{port}");
let hostname_ = hostname.clone();
let bind = format!("127.0.0.1:{port}").parse().unwrap();
let config = IbisConfig {
bind,
database_url,
registration_open: true,
federation: IbisConfigFederation {
domain: hostname.clone(),
..Default::default()
},
..Default::default()
};
let handle = tokio::task::spawn(async move {
start(&hostname_, &db_url).await.unwrap();
start(config).await.unwrap();
});
// wait a moment for the backend to start
tokio::time::sleep(Duration::from_millis(100)).await;

View File

@ -25,5 +25,5 @@ echo "$PGHOST/.s.PGSQL.5432"
pg_ctl start --options="-c listen_addresses= -c unix_socket_directories=$PGHOST"
# Setup database
psql -c "CREATE USER lemmy WITH PASSWORD 'password' SUPERUSER;" -U postgres
psql -c "CREATE DATABASE lemmy WITH OWNER lemmy;" -U postgres
psql -c "CREATE USER ibis WITH PASSWORD 'password' SUPERUSER;" -U postgres
psql -c "CREATE DATABASE ibis WITH OWNER ibis;" -U postgres

View File

@ -98,32 +98,23 @@ async fn test_follow_instance() -> MyResult<()> {
let data = TestData::start().await;
// check initial state
let alpha_instance = data.alpha.get_local_instance().await?;
assert_eq!(0, alpha_instance.followers.len());
assert_eq!(0, alpha_instance.following.len());
let alpha_user = data.alpha.my_profile().await?;
assert_eq!(0, alpha_user.following.len());
let beta_instance = data.beta.get_local_instance().await?;
assert_eq!(0, beta_instance.followers.len());
assert_eq!(0, beta_instance.following.len());
data.alpha.follow_instance(&data.beta.hostname).await?;
dbg!(&data.alpha.hostname, &data.beta.hostname);
// check that follow was federated
let alpha_instance = data.alpha.get_local_instance().await?;
assert_eq!(1, alpha_instance.following.len());
assert_eq!(0, alpha_instance.followers.len());
assert_eq!(
beta_instance.instance.ap_id,
alpha_instance.following[0].ap_id
);
let alpha_user = data.alpha.my_profile().await?;
dbg!(&alpha_user);
assert_eq!(1, alpha_user.following.len());
assert_eq!(beta_instance.instance.ap_id, alpha_user.following[0].ap_id);
let beta_instance = data.beta.get_local_instance().await?;
assert_eq!(0, beta_instance.following.len());
assert_eq!(1, beta_instance.followers.len());
// TODO: compare full ap_id of alpha user, but its not available through api yet
assert_eq!(
alpha_instance.instance.ap_id.inner().domain(),
beta_instance.followers[0].ap_id.inner().domain()
);
assert_eq!(alpha_user.person.ap_id, beta_instance.followers[0].ap_id);
data.stop()
}