diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 8ada459..968a4d8 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1033,6 +1033,7 @@ dependencies = [ "tracing-subscriber", "twapi-ureq", "ureq", + "url 2.1.1", "uuid", ] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 9dc5f28..7b90fb6 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -9,27 +9,28 @@ edition = "2018" [dependencies] chrono = { version = "0.4", features = ["serde"] } color-eyre = "0.5" -twapi-ureq = "0.1.5" +futures-io = "0.3" +hex = "0.4" kankyo = "0.3" log = "0.4" mime = "0.3.0" paseto = { version = "1.0", features = ["easy_tokens", "v2"] } -ring = { version = "^0.16", features = ["std"] } +prometheus = { version = "0.10", default-features = false, features = ["process"] } rand = "0" +ring = { version = "^0.16", features = ["std"] } +rocket = "0.4" +rocket_prometheus = "0.7.0" rusty_ulid = "0.10" serde_json = "^1" serde = { version = "1", features = ["derive"] } thiserror = "1" -rocket = "0.4" tracing = "0.1" tracing-log = "0.1" tracing-subscriber = "0.2" +twapi-ureq = "0.1.5" ureq = { version = "1", features = ["json", "charset"] } uuid = { version = "0.7", features = ["serde", "v4"] } -rocket_prometheus = "0.7.0" -prometheus = { version = "0.10", default-features = false, features = ["process"] } -futures-io = "0.3" -hex = "0.4" +url = "2" jsonfeed = { git = "https://github.com/Xe/site" } diff --git a/backend/migrations/2020-11-04-163029_webmention-unique/down.sql b/backend/migrations/2020-11-04-163029_webmention-unique/down.sql new file mode 100644 index 0000000..50c314d --- /dev/null +++ b/backend/migrations/2020-11-04-163029_webmention-unique/down.sql @@ -0,0 +1 @@ +DROP INDEX webmentions_source_target; diff --git a/backend/migrations/2020-11-04-163029_webmention-unique/up.sql b/backend/migrations/2020-11-04-163029_webmention-unique/up.sql new file mode 100644 index 0000000..757eee5 --- /dev/null +++ b/backend/migrations/2020-11-04-163029_webmention-unique/up.sql @@ -0,0 +1,2 @@ +CREATE UNIQUE INDEX webmentions_source_target + ON webmentions(source_url, target_url); diff --git a/backend/src/api/mod.rs b/backend/src/api/mod.rs index 443f717..2095adb 100644 --- a/backend/src/api/mod.rs +++ b/backend/src/api/mod.rs @@ -13,6 +13,7 @@ use rocket_contrib::json::Json; use std::io::Read; pub mod switch; +pub mod webmention; #[get("/members")] #[instrument(skip(conn), err)] @@ -79,6 +80,12 @@ pub enum Error { #[error("web API interop error: {0}")] Web(#[from] web::Error), + + #[error("URL parsing error: {0}")] + URL(#[from] url::ParseError), + + #[error("invalid webmention: {0}")] + InvalidWebMention(String), } pub type Result = std::result::Result; @@ -88,6 +95,7 @@ impl<'a> Responder<'a> for Error { error!("{}", self); match self { Error::NotFound => Err(Status::NotFound), + Error::InvalidWebMention(_) => Err(Status::BadRequest), _ => Err(Status::InternalServerError), } } diff --git a/backend/src/api/webmention.rs b/backend/src/api/webmention.rs new file mode 100644 index 0000000..62e7106 --- /dev/null +++ b/backend/src/api/webmention.rs @@ -0,0 +1,107 @@ +use super::{Error, Result}; +use crate::{models, schema, MainDatabase}; +use diesel::prelude::*; +use rocket::{ + request::Form, + response::{self, Responder}, + Request, Response, +}; +use rocket_contrib::json::Json; +use rusty_ulid::generate_ulid_string; +use url::Url; + +#[derive(FromForm, Debug)] +pub struct WebMention { + source: String, + target: String, +} + +impl WebMention { + fn check(&self) -> Result { + if self.source == self.target { + return Err(Error::InvalidWebMention("source == target".into())); + } + + let u: Url = Url::parse(&self.source)?; + match u.scheme() { + "http" | "https" => {} + _ => return Err(Error::InvalidWebMention("invalid source scheme".into())), + } + + u.host_str() + .ok_or(Error::InvalidWebMention("no host found in target".into()))?; + + let u: Url = Url::parse(&self.target)?; + match u.scheme() { + "http" | "https" => {} + _ => return Err(Error::InvalidWebMention("invalid target scheme".into())), + } + + match u + .host_str() + .ok_or(Error::InvalidWebMention("no host found in target".into()))? + { + "christine.website" | "cetacean.club" => {} + _ => return Err(Error::InvalidWebMention("invalid target host".into())), + } + + Ok(()) + } +} + +impl Into for WebMention { + fn into(self) -> models::WebMention { + models::WebMention { + id: generate_ulid_string(), + source_url: self.source, + target_url: self.target, + } + } +} + +impl<'a> Responder<'a> for models::WebMention { + fn respond_to(self, _: &Request) -> response::Result<'a> { + Response::build() + .raw_header( + "Location", + format!("https://mi.christine.website/api/webmention/{}", self.id), + ) + .ok() + } +} + +#[post("/webmention/accept", data = "")] +#[instrument(skip(conn, mention), err)] +pub fn accept(conn: MainDatabase, mention: Form) -> Result { + use schema::webmentions; + + let mention = mention.into_inner(); + mention.check()?; + + info!( + source = &mention.source[..], + target = &mention.target[..], + "webmention received" + ); + + let wm: models::WebMention = mention.into(); + diesel::insert_into(webmentions::table) + .values(&wm) + .execute(&*conn) + .map_err(Error::Database)?; + + Ok(wm) +} + +#[get("/webmention/")] +#[instrument(skip(conn), err)] +pub fn get(conn: MainDatabase, mention_id: String) -> Result> { + use schema::webmentions::dsl::webmentions; + + Ok(Json( + webmentions + .find(mention_id) + .get_result(&*conn) + .map_err(Error::Database)?, + )) +} diff --git a/backend/src/main.rs b/backend/src/main.rs index 3cf532b..adf88f4 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -57,6 +57,8 @@ fn main() -> Result<()> { api::switch::get, api::switch::list, api::switch::switch, + api::webmention::accept, + api::webmention::get, api::get_members, api::token_info, api::tweet, diff --git a/backend/src/models.rs b/backend/src/models.rs index 7773ff5..2177f3f 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -50,3 +50,11 @@ pub struct NewSwitch { pub struct UpdateSwitchTime { pub ended_at: Option, } + +#[derive(Queryable, Associations, Insertable, Serialize)] +#[table_name = "webmentions"] +pub struct WebMention { + pub id: String, + pub source_url: String, + pub target_url: String, +}