diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 00000000..4d3e4f37 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,26 @@ +codecov: + notify: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "70...100" + + status: + project: no + patch: no + changes: no + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "header, diff" + behavior: default + require_changes: no diff --git a/.travis.yml b/.travis.yml index 54a59496..b8238289 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,12 +29,12 @@ jobs: name: "Build with postgresql" env: - MIGRATION_DIR=migrations/postgres FEATURES=postgres DATABASE_URL=postgres://postgres@localhost/plume - script: cargo build --no-default-features --features="${FEATURES}" + script: cargo build --no-default-features --features="${FEATURES}" --release - stage: build name: "Build with sqlite" env: - MIGRATION_DIR=migrations/sqlite FEATURES=sqlite DATABASE_URL=plume.sqlite3 - script: cargo build --no-default-features --features="${FEATURES}" + script: cargo build --no-default-features --features="${FEATURES}" --release - stage: test and coverage name: "Test with potgresql backend" env: diff --git a/Dockerfile b/Dockerfile index bcc3a98d..fdc9103b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,8 +14,8 @@ WORKDIR /app COPY Cargo.toml Cargo.lock ./ RUN cargo install diesel_cli --no-default-features --features postgres --version '=1.3.0' COPY . . -RUN cargo install --force --no-default-features --features postgres +RUN cargo install --path ./ --force --no-default-features --features postgres RUN cargo install --path plume-cli --force --no-default-features --features postgres -RUN rm -rf target/debug/incremental +RUN rm -rf target/release/incremental CMD ["plume"] EXPOSE 7878 diff --git a/docs/INSTALL.md b/docs/INSTALL.md index cf2f2af2..70996bfc 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -182,7 +182,7 @@ When in doubt, run them. Then, you'll need to install Plume and the CLI tools to manage your instance. ``` -cargo install --no-default-features --features $FEATURES +cargo install --no-default-features --features $FEATURES --path ./ cargo install --no-default-features --features $FEATURES --path plume-cli ``` diff --git a/plume-common/src/activity_pub/request.rs b/plume-common/src/activity_pub/request.rs index eea7dcf3..e569d2bc 100644 --- a/plume-common/src/activity_pub/request.rs +++ b/plume-common/src/activity_pub/request.rs @@ -41,6 +41,10 @@ impl Digest { } } + pub fn verify_header(&self, other: &Digest) -> bool { + self.value()==other.value() + } + pub fn algorithm(&self) -> &str { let pos = self .0 @@ -69,6 +73,13 @@ impl Digest { Err(()) } } + + pub fn from_body(body: &str) -> Self { + let mut hasher = Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error"); + hasher.update(body.as_bytes()).expect("Digest::digest: content insertion error"); + let res = base64::encode(&hasher.finish().expect("Digest::digest: finalizing error")); + Digest(format!("SHA-256={}", res)) + } } pub fn headers() -> HeaderMap { diff --git a/plume-common/src/activity_pub/sign.rs b/plume-common/src/activity_pub/sign.rs index 635ca357..94368ce0 100644 --- a/plume-common/src/activity_pub/sign.rs +++ b/plume-common/src/activity_pub/sign.rs @@ -129,7 +129,7 @@ impl SignatureValidity { pub fn verify_http_headers( sender: &S, all_headers: &HeaderMap, - data: &str, + data: &request::Digest, ) -> SignatureValidity { let sig_header = all_headers.get_one("Signature"); if sig_header.is_none() { @@ -176,7 +176,7 @@ pub fn verify_http_headers( } let digest = all_headers.get_one("digest").unwrap_or(""); let digest = request::Digest::from_header(digest); - if !digest.map(|d| d.verify(&data)).unwrap_or(false) { + if !digest.map(|d| d.verify_header(&data)).unwrap_or(false) { // signature was valid, but body content does not match its digest return SignatureValidity::Invalid; } diff --git a/src/inbox.rs b/src/inbox.rs index a045d7fc..9a2425fa 100644 --- a/src/inbox.rs +++ b/src/inbox.rs @@ -11,11 +11,21 @@ use activitypub::{ object::Tombstone }; use failure::Error; +use rocket::{ + data::*, + http::Status, + Outcome::{self, *}, + Request, +}; +use rocket_contrib::json::*; +use serde::Deserialize; use serde_json; +use std::io::Read; + use plume_common::activity_pub::{ inbox::{Deletable, FromActivity, InboxError}, - Id, + Id,request::Digest, }; use plume_models::{ comments::Comment, follows::Follow, instance::Instance, likes, posts::Post, reshares::Reshare, @@ -125,3 +135,36 @@ pub trait Inbox { impl Inbox for Instance {} impl Inbox for User {} + +const JSON_LIMIT: u64 = 1 << 20; + +pub struct SignedJson(pub Digest, pub Json); + +impl<'a, T: Deserialize<'a>> FromData<'a> for SignedJson { + type Error = JsonError<'a>; + type Owned = String; + type Borrowed = str; + + fn transform(r: &Request, d: Data) -> Transform> { + let size_limit = r.limits().get("json").unwrap_or(JSON_LIMIT); + let mut s = String::with_capacity(512); + match d.open().take(size_limit).read_to_string(&mut s) { + Ok(_) => Transform::Borrowed(Success(s)), + Err(e) => Transform::Borrowed(Failure((Status::BadRequest, JsonError::Io(e)))) + } + } + + fn from_data(_: &Request, o: Transformed<'a, Self>) -> Outcome { + let string = o.borrowed()?; + match serde_json::from_str(&string) { + Ok(v) => Success(SignedJson(Digest::from_body(&string),Json(v))), + Err(e) => { + if e.is_data() { + Failure((Status::UnprocessableEntity, JsonError::Parse(string, e))) + } else { + Failure((Status::BadRequest, JsonError::Parse(string, e))) + } + } + } + } +} diff --git a/src/routes/instance.rs b/src/routes/instance.rs index d8b7f69e..68f05b90 100644 --- a/src/routes/instance.rs +++ b/src/routes/instance.rs @@ -16,7 +16,7 @@ use plume_models::{ safe_string::SafeString, instance::* }; -use inbox::Inbox; +use inbox::{Inbox, SignedJson}; use routes::Page; use template_utils::Ructe; use Searcher; @@ -186,15 +186,15 @@ pub fn ban(_admin: Admin, conn: DbConn, id: i32, searcher: Searcher) -> Redirect } #[post("/inbox", data = "")] -pub fn shared_inbox(conn: DbConn, data: String, headers: Headers, searcher: Searcher) -> Result> { - let act: serde_json::Value = serde_json::from_str(&data[..]).expect("instance::shared_inbox: deserialization error"); +pub fn shared_inbox(conn: DbConn, data: SignedJson, headers: Headers, searcher: Searcher) -> Result> { + let act = data.1.into_inner(); let activity = act.clone(); let actor_id = activity["actor"].as_str() .or_else(|| activity["actor"]["id"].as_str()).ok_or(status::BadRequest(Some("Missing actor id for activity")))?; let actor = User::from_url(&conn, actor_id).expect("instance::shared_inbox: user error"); - if !verify_http_headers(&actor, &headers.0, &data).is_secure() && + if !verify_http_headers(&actor, &headers.0, &data.0).is_secure() && !act.clone().verify(&actor) { println!("Rejected invalid activity supposedly from {}, with headers {:?}", actor.username, headers.0); return Err(status::BadRequest(Some("Invalid signature"))); diff --git a/src/routes/user.rs b/src/routes/user.rs index 70d68bbe..ae96513e 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -9,7 +9,7 @@ use rocket_i18n::I18n; use serde_json; use validator::{Validate, ValidationError, ValidationErrors}; -use inbox::Inbox; +use inbox::{Inbox, SignedJson}; use plume_common::activity_pub::{ broadcast, inbox::{Deletable, FromActivity, Notify}, @@ -349,13 +349,12 @@ pub fn outbox(name: String, conn: DbConn) -> Option, headers: Headers, searcher: Searcher, ) -> Result>> { let user = User::find_local(&*conn, &name).ok_or(None)?; - let act: serde_json::Value = - serde_json::from_str(&data).expect("user::inbox: deserialization error"); + let act = data.1.into_inner(); let activity = act.clone(); let actor_id = activity["actor"] @@ -366,7 +365,7 @@ pub fn inbox( ))))?; let actor = User::from_url(&conn, actor_id).expect("user::inbox: user error"); - if !verify_http_headers(&actor, &headers.0, &data).is_secure() + if !verify_http_headers(&actor, &headers.0, &data.0).is_secure() && !act.clone().verify(&actor) { println!(