Compare commits

..
Author SHA1 Message Date
Trinity Pointard f0ca7ccde8 attempt to add support for ldap
Blind attempt
Don't create account for existing ldap that is unknown yet
Include connection pooling
2019-08-16 19:06:07 +02:00
Trinity Pointard 7fd1fe6d52 refactor login
refactor login so it's self contained in a single function
will be useful in adding other password based login method such as ldap
2019-08-16 19:06:06 +02:00
34 changed files with 627 additions and 1239 deletions
Generated
+286 -340
View File
File diff suppressed because it is too large Load Diff
+5 -6
View File
@@ -18,21 +18,20 @@ guid-create = "0.1"
heck = "0.3.0"
lettre = { git = "https://github.com/lettre/lettre", rev = "c988b1760ad8179d9e7f3fb8594d2b86cf2a0a49" }
lettre_email = { git = "https://github.com/lettre/lettre", rev = "c988b1760ad8179d9e7f3fb8594d2b86cf2a0a49" }
num_cpus = "1.10"
reqwest = "0.9"
num_cpus = "1.0"
rocket = "0.4.0"
rocket_contrib = { version = "0.4.0", features = ["json"] }
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
rpassword = "4.0"
rpassword = "3.0"
runtime-fmt = "0.3.0"
scheduled-thread-pool = "0.2.2"
scheduled-thread-pool = "0.2.0"
serde = "1.0"
serde_json = "1.0"
serde_qs = "0.5"
serde_qs = "0.4"
shrinkwraprs = "0.2.1"
validator = "0.8"
validator_derive = "0.8"
webfinger = "0.4.1"
webfinger = "0.3.1"
[[bin]]
name = "plume"
@@ -1,2 +0,0 @@
-- undo the adding of custom_domain column to blogs table.
ALTER TABLE blogs DROP COLUMN custom_domain;
@@ -1,2 +0,0 @@
--- Adding custom domain to Blog as an optional field
ALTER TABLE blogs ADD COLUMN custom_domain VARCHAR DEFAULT NULL UNIQUE;
@@ -1,56 +0,0 @@
-- undo the adding of "custom_domain" to blogs
CREATE TABLE IF NOT EXISTS "blogs_drop_custom_domain" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
actor_id VARCHAR NOT NULL,
title VARCHAR NOT NULL,
summary TEXT NOT NULL DEFAULT '',
outbox_url VARCHAR NOT NULL UNIQUE,
inbox_url VARCHAR NOT NULL UNIQUE,
instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL,
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
ap_url text not null default '' UNIQUE,
private_key TEXT,
public_key TEXT NOT NULL DEFAULT '',
fqn TEXT NOT NULL DEFAULT '',
summary_html TEXT NOT NULL DEFAULT '',
icon_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL,
banner_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL,
CONSTRAINT blog_unique UNIQUE (actor_id, instance_id)
);
INSERT INTO blogs_drop_custom_domain (
id,
actor_id,
title,
summary,
outbox_url,
inbox_url,
instance_id,
creation_date,
ap_url,
private_key,
public_key,
fqn,
summary_html,
icon_id,
banner_id
) SELECT
id,
actor_id,
title,
summary,
outbox_url,
inbox_url,
instance_id,
creation_date,
ap_url,
private_key,
public_key,
fqn,
summary_html,
icon_id,
banner_id
FROM blogs;
DROP TABLE blogs;
ALTER TABLE "blogs_drop_custom_domain" RENAME to blogs;
@@ -1,57 +0,0 @@
-- add custom_domain to blogs
CREATE TABLE IF NOT EXISTS "blogs_add_custom_domain" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
actor_id VARCHAR NOT NULL,
title VARCHAR NOT NULL,
summary TEXT NOT NULL DEFAULT '',
outbox_url VARCHAR NOT NULL UNIQUE,
inbox_url VARCHAR NOT NULL UNIQUE,
instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL,
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
ap_url text not null default '' UNIQUE,
private_key TEXT,
public_key TEXT NOT NULL DEFAULT '',
fqn TEXT NOT NULL DEFAULT '',
summary_html TEXT NOT NULL DEFAULT '',
icon_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL,
banner_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL,
custom_domain text default NULL UNIQUE,
CONSTRAINT blog_unique UNIQUE (actor_id, instance_id)
);
INSERT INTO blogs_add_custom_domain (
id,
actor_id,
title,
summary,
outbox_url,
inbox_url,
instance_id,
creation_date,
ap_url,
private_key,
public_key,
fqn,
summary_html,
icon_id,
banner_id
) SELECT
id,
actor_id,
title,
summary,
outbox_url,
inbox_url,
instance_id,
creation_date,
ap_url,
private_key,
public_key,
fqn,
summary_html,
icon_id,
banner_id
FROM blogs;
DROP TABLE blogs;
ALTER TABLE "blogs_add_custom_domain" RENAME to blogs;
+1 -1
View File
@@ -10,7 +10,7 @@ path = "src/main.rs"
[dependencies]
clap = "2.33"
dotenv = "0.14"
rpassword = "4.0"
rpassword = "3.0"
[dependencies.diesel]
features = ["r2d2", "chrono"]
+2 -10
View File
@@ -109,16 +109,8 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
rpassword::read_password().expect("Couldn't read your password.")
});
NewUser::new_local(
conn,
username,
display_name,
admin,
&bio,
email,
User::hash_pass(&password).expect("Couldn't hash password"),
)
.expect("Couldn't save new user");
NewUser::new_local(conn, username, display_name, admin, &bio, email, &password)
.expect("Couldn't save new user");
}
fn reset_password<'a>(args: &ArgMatches<'a>, conn: &Connection) {
+3 -3
View File
@@ -5,13 +5,13 @@ authors = ["Plume contributors"]
[dependencies]
activitypub = "0.1.1"
activitystreams-derive = "0.1.1"
activitystreams-derive = "0.1.0"
activitystreams-traits = "0.1.0"
array_tool = "1.0"
base64 = "0.10"
heck = "0.3.0"
hex = "0.3"
hyper = "0.12.33"
hyper = "0.12.28"
openssl = "0.10.22"
rocket = "0.4.0"
reqwest = "0.9"
@@ -19,7 +19,7 @@ serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
shrinkwraprs = "0.2.1"
tokio = "0.1.22"
tokio = "0.1.21"
[dependencies.chrono]
features = ["serde"]
+2 -2
View File
@@ -4,8 +4,8 @@ version = "0.3.0"
authors = ["Plume contributors"]
[dependencies]
stdweb = "=0.4.18"
stdweb-internal-runtime = "=0.1.4"
stdweb = "=0.4.14"
stdweb-internal-runtime = "=0.1.3"
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
+7 -5
View File
@@ -7,24 +7,26 @@ authors = ["Plume contributors"]
activitypub = "0.1.1"
ammonia = "2.1.1"
askama_escape = "0.1"
bcrypt = "0.5"
bcrypt = "0.4"
guid-create = "0.1"
heck = "0.3.0"
itertools = "0.8.0"
lazy_static = "*"
ldap3 = "0.6.1"
migrations_internals= "1.4.0"
openssl = "0.10.22"
rocket = "0.4.0"
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
reqwest = "0.9"
scheduled-thread-pool = "0.2.2"
scheduled-thread-pool = "0.2.0"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
tantivy = "0.10.1"
url = "2.1"
webfinger = "0.4.1"
tantivy = "0.9.1"
url = "1.7"
webfinger = "0.3.1"
whatlang = "0.7.1"
shrinkwraprs = "0.2.1"
diesel-derive-newtype = "0.1.2"
[dependencies.chrono]
+1 -121
View File
@@ -7,11 +7,6 @@ use openssl::{
rsa::Rsa,
sign::{Signer, Verifier},
};
use rocket::{
http::RawStr,
outcome::IntoOutcome,
request::{self, FromFormValue, FromRequest, Request},
};
use serde_json;
use url::Url;
use webfinger::*;
@@ -26,70 +21,11 @@ use posts::Post;
use safe_string::SafeString;
use schema::blogs;
use search::Searcher;
use std::default::Default;
use std::fmt::{self, Display};
use std::ops::Deref;
use std::sync::RwLock;
use users::User;
use {Connection, Error, PlumeRocket, Result};
pub type CustomGroup = CustomObject<ApSignature, Group>;
#[derive(Clone, Debug, PartialEq, DieselNewType)]
pub struct Host(String);
impl Host {
pub fn new(host: impl ToString) -> Host {
Host(host.to_string())
}
}
impl Deref for Host {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl Display for Host {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for Host {
fn as_ref(&self) -> &str {
&self.0
}
}
impl<'a, 'r> FromRequest<'a, 'r> for Host {
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<Host, ()> {
request
.headers()
.get_one("Host")
.and_then(|x| {
if Blog::list_custom_domains().contains(&x.to_string()) {
Some(Host::new(x))
} else {
None
}
})
.or_forward(())
}
}
impl<'v> FromFormValue<'v> for Host {
type Error = &'v RawStr;
fn from_form_value(form_value: &'v RawStr) -> std::result::Result<Host, &'v RawStr> {
let val = String::from_form_value(form_value)?;
Ok(Host::new(&val))
}
}
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
#[changeset_options(treat_none_as_null = "true")]
pub struct Blog {
@@ -108,7 +44,6 @@ pub struct Blog {
pub summary_html: SafeString,
pub icon_id: Option<i32>,
pub banner_id: Option<i32>,
pub custom_domain: Option<Host>,
}
#[derive(Default, Insertable)]
@@ -126,15 +61,10 @@ pub struct NewBlog {
pub summary_html: SafeString,
pub icon_id: Option<i32>,
pub banner_id: Option<i32>,
pub custom_domain: Option<Host>,
}
const BLOG_PREFIX: &str = "~";
lazy_static! {
static ref CUSTOM_DOMAINS: RwLock<Vec<String>> = RwLock::new(vec![]);
}
impl Blog {
insert!(blogs, NewBlog, |inserted, conn| {
let instance = inserted.get_instance(conn)?;
@@ -214,15 +144,8 @@ impl Blog {
}
}
pub fn find_by_host(c: &PlumeRocket, host: Host) -> Result<Blog> {
blogs::table
.filter(blogs::custom_domain.eq(host))
.first::<Blog>(&*c.conn)
.map_err(|_| Error::NotFound)
}
fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<Blog> {
resolve_with_prefix(Prefix::Group, acct.to_owned(), true)?
resolve(acct.to_owned(), true)?
.links
.into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
@@ -346,19 +269,6 @@ impl Blog {
})
}
pub fn url(&self) -> String {
format!(
"https://{}",
self.custom_domain
.clone()
.unwrap_or_else(|| Host::new(format!(
"{}/~/{}",
Instance::get_local().unwrap().public_domain,
self.fqn,
)))
)
}
pub fn icon_url(&self, conn: &Connection) -> String {
self.icon_id
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
@@ -380,23 +290,6 @@ impl Blog {
.map(|_| ())
.map_err(Error::from)
}
pub fn list_custom_domains() -> Vec<String> {
CUSTOM_DOMAINS.read().unwrap().clone()
}
pub fn cache_custom_domains(conn: &Connection) {
*CUSTOM_DOMAINS.write().unwrap() = Blog::list_custom_domains_uncached(conn).unwrap();
}
pub fn list_custom_domains_uncached(conn: &Connection) -> Result<Vec<String>> {
blogs::table
.filter(blogs::custom_domain.is_not_null())
.select(blogs::custom_domain)
.load::<Option<String>>(conn)
.map_err(Error::from)
.map(|res| res.into_iter().map(Option::unwrap).collect::<Vec<_>>())
}
}
impl IntoId for Blog {
@@ -499,7 +392,6 @@ impl FromId<PlumeRocket> for Blog {
.summary_string()
.unwrap_or_default(),
),
custom_domain: None,
},
)
}
@@ -549,7 +441,6 @@ impl NewBlog {
title: String,
summary: String,
instance_id: i32,
custom_domain: Option<Host>,
) -> Result<NewBlog> {
let (pub_key, priv_key) = sign::gen_keypair();
Ok(NewBlog {
@@ -559,7 +450,6 @@ impl NewBlog {
instance_id,
public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?,
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
custom_domain,
..NewBlog::default()
})
}
@@ -587,7 +477,6 @@ pub(crate) mod tests {
"Blog name".to_owned(),
"This is a small blog".to_owned(),
Instance::get_local().unwrap().id,
None,
)
.unwrap(),
)
@@ -599,7 +488,6 @@ pub(crate) mod tests {
"My blog".to_owned(),
"Welcome to my blog".to_owned(),
Instance::get_local().unwrap().id,
Some(Host::new("blog.myname.me")),
)
.unwrap(),
)
@@ -611,7 +499,6 @@ pub(crate) mod tests {
"Why I like Plume".to_owned(),
"In this blog I will explay you why I like Plume so much".to_owned(),
Instance::get_local().unwrap().id,
None,
)
.unwrap(),
)
@@ -672,7 +559,6 @@ pub(crate) mod tests {
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::get_local().unwrap().id,
Some(Host::new("some.blog.com")),
)
.unwrap(),
)
@@ -701,7 +587,6 @@ pub(crate) mod tests {
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::get_local().unwrap().id,
None,
)
.unwrap(),
)
@@ -713,7 +598,6 @@ pub(crate) mod tests {
"Blog".to_owned(),
"I've named my blog Blog".to_owned(),
Instance::get_local().unwrap().id,
Some(Host::new("named.example.blog")),
)
.unwrap(),
)
@@ -806,7 +690,6 @@ pub(crate) mod tests {
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::get_local().unwrap().id,
None,
)
.unwrap(),
)
@@ -831,7 +714,6 @@ pub(crate) mod tests {
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::get_local().unwrap().id,
Some(Host::new("some.blog.com")),
)
.unwrap(),
)
@@ -870,7 +752,6 @@ pub(crate) mod tests {
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::get_local().unwrap().id,
None,
)
.unwrap(),
)
@@ -882,7 +763,6 @@ pub(crate) mod tests {
"Blog".to_owned(),
"I've named my blog Blog".to_owned(),
Instance::get_local().unwrap().id,
Some(Host::new("my.blog.com")),
)
.unwrap(),
)
+23
View File
@@ -14,6 +14,7 @@ pub struct Config {
pub search_index: String,
pub rocket: Result<RocketConfig, RocketError>,
pub logo: LogoConfig,
pub ldap: LdapConfig,
}
#[derive(Debug, Clone)]
@@ -184,6 +185,27 @@ impl Default for LogoConfig {
}
}
#[derive(Debug, Clone)]
pub struct LdapConfig {
pub url: Option<String>,
pub bind_dn: Option<String>,
}
impl Default for LdapConfig {
fn default() -> Self {
let url = var("LDAP_URL").ok();
let bind_dn = var("LDAP_BIND_DN").ok();
if url.is_some() ^ bind_dn.is_some() {
panic!(
r#"Invalid configuration :
You must provide both LDAP_URL and LDAP_BIND_DN, or neither"#
);
} else {
LdapConfig { url, bind_dn }
}
}
}
lazy_static! {
pub static ref CONFIG: Config = Config {
base_url: var("BASE_URL").unwrap_or_else(|_| format!(
@@ -199,5 +221,6 @@ lazy_static! {
search_index: var("SEARCH_INDEX").unwrap_or_else(|_| "search_index".to_owned()),
rocket: get_rocket_config(),
logo: LogoConfig::default(),
ldap: LdapConfig::default(),
};
}
+80
View File
@@ -0,0 +1,80 @@
use crate::CONFIG;
use ldap3::LdapConn;
use std::io;
use std::sync::{mpsc, Mutex};
use std::thread;
type Message = (String, String, mpsc::Sender<io::Result<bool>>);
pub struct Ldap {
channel: mpsc::Sender<Message>,
}
impl Ldap {
pub fn get_shared() -> Self {
Ldap {
channel: CHANNEL.lock().unwrap().clone(),
}
}
pub fn connect(&self, username: String, password: String) -> LdapResult {
let (s, r) = mpsc::channel();
self.channel.send((username, password, s)).unwrap(); //we know the remote end was not closed
LdapResult { channel: r }
}
}
pub struct LdapResult {
channel: mpsc::Receiver<io::Result<bool>>,
}
impl LdapResult {
pub fn get(self) -> io::Result<bool> {
self.channel.recv().unwrap() //we know some message must have been send, be it an error
}
}
/// This function loop indefinitelly, handling requests
fn handle(url: &str, bind_dn: &str, channel: mpsc::Receiver<Message>) {
let mut conn = LdapConn::new(url).expect("Error connecting to ldap server");
for (user, password, channel) in channel.iter() {
let res = conn
.simple_bind(&format!("uid={},{}", user, bind_dn), &password)
.map(|r| r.rc == 0);
let err = res.is_err();
channel.send(res).ok(); //we can't assume the other end did not drop it's handle
let err = conn.unbind().is_err() || err;
if err {
if let Ok(c) = LdapConn::new(url) {
conn = c;
}
}
}
}
fn ignore(channel: mpsc::Receiver<Message>) {
for (_user, _password, channel) in channel.iter() {
channel.send(Ok(false)).ok();
}
}
lazy_static! {
static ref CHANNEL: Mutex<mpsc::Sender<Message>> = {
let (s, r) = mpsc::channel();
let builder = thread::Builder::new().name("ldap_handler".into());
builder
.spawn(move || {
if CONFIG.ldap.url.is_some() && CONFIG.ldap.bind_dn.is_some() {
handle(
CONFIG.ldap.url.as_ref().unwrap(),
CONFIG.ldap.bind_dn.as_ref().unwrap(),
r,
)
} else {
ignore(r);
}
})
.unwrap();
Mutex::new(s)
};
}
+2 -2
View File
@@ -10,13 +10,12 @@ extern crate bcrypt;
extern crate chrono;
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_derive_newtype;
extern crate guid_create;
extern crate heck;
extern crate itertools;
#[macro_use]
extern crate lazy_static;
extern crate ldap3;
extern crate migrations_internals;
extern crate openssl;
extern crate plume_api;
@@ -365,6 +364,7 @@ pub mod follows;
pub mod headers;
pub mod inbox;
pub mod instance;
pub mod ldap;
pub mod likes;
pub mod medias;
pub mod mentions;
+76 -77
View File
@@ -1,17 +1,17 @@
table! {
api_tokens (id) {
id -> Integer,
id -> Int4,
creation_date -> Timestamp,
value -> Text,
scopes -> Text,
app_id -> Integer,
user_id -> Integer,
app_id -> Int4,
user_id -> Int4,
}
}
table! {
apps (id) {
id -> Integer,
id -> Int4,
name -> Text,
client_id -> Text,
client_secret -> Text,
@@ -23,71 +23,70 @@ table! {
table! {
blog_authors (id) {
id -> Integer,
blog_id -> Integer,
author_id -> Integer,
id -> Int4,
blog_id -> Int4,
author_id -> Int4,
is_owner -> Bool,
}
}
table! {
blogs (id) {
id -> Integer,
actor_id -> Text,
title -> Text,
id -> Int4,
actor_id -> Varchar,
title -> Varchar,
summary -> Text,
outbox_url -> Text,
inbox_url -> Text,
instance_id -> Integer,
outbox_url -> Varchar,
inbox_url -> Varchar,
instance_id -> Int4,
creation_date -> Timestamp,
ap_url -> Text,
private_key -> Nullable<Text>,
public_key -> Text,
fqn -> Text,
summary_html -> Text,
icon_id -> Nullable<Integer>,
banner_id -> Nullable<Integer>,
custom_domain -> Nullable<Text>,
}
}
table! {
comment_seers (id) {
id -> Integer,
comment_id -> Integer,
user_id -> Integer,
icon_id -> Nullable<Int4>,
banner_id -> Nullable<Int4>,
}
}
table! {
comments (id) {
id -> Integer,
id -> Int4,
content -> Text,
in_response_to_id -> Nullable<Integer>,
post_id -> Integer,
author_id -> Integer,
in_response_to_id -> Nullable<Int4>,
post_id -> Int4,
author_id -> Int4,
creation_date -> Timestamp,
ap_url -> Nullable<Text>,
ap_url -> Nullable<Varchar>,
sensitive -> Bool,
spoiler_text -> Text,
public_visibility -> Bool,
}
}
table! {
comment_seers (id) {
id -> Int4,
comment_id -> Int4,
user_id -> Int4,
}
}
table! {
follows (id) {
id -> Integer,
follower_id -> Integer,
following_id -> Integer,
id -> Int4,
follower_id -> Int4,
following_id -> Int4,
ap_url -> Text,
}
}
table! {
instances (id) {
id -> Integer,
public_domain -> Text,
name -> Text,
id -> Int4,
public_domain -> Varchar,
name -> Varchar,
local -> Bool,
blocked -> Bool,
creation_date -> Timestamp,
@@ -95,50 +94,50 @@ table! {
short_description -> Text,
long_description -> Text,
default_license -> Text,
long_description_html -> Text,
short_description_html -> Text,
long_description_html -> Varchar,
short_description_html -> Varchar,
}
}
table! {
likes (id) {
id -> Integer,
user_id -> Integer,
post_id -> Integer,
id -> Int4,
user_id -> Int4,
post_id -> Int4,
creation_date -> Timestamp,
ap_url -> Text,
ap_url -> Varchar,
}
}
table! {
medias (id) {
id -> Integer,
id -> Int4,
file_path -> Text,
alt_text -> Text,
is_remote -> Bool,
remote_url -> Nullable<Text>,
sensitive -> Bool,
content_warning -> Nullable<Text>,
owner_id -> Integer,
owner_id -> Int4,
}
}
table! {
mentions (id) {
id -> Integer,
mentioned_id -> Integer,
post_id -> Nullable<Integer>,
comment_id -> Nullable<Integer>,
id -> Int4,
mentioned_id -> Int4,
post_id -> Nullable<Int4>,
comment_id -> Nullable<Int4>,
}
}
table! {
notifications (id) {
id -> Integer,
user_id -> Integer,
id -> Int4,
user_id -> Int4,
creation_date -> Timestamp,
kind -> Text,
object_id -> Integer,
kind -> Varchar,
object_id -> Int4,
}
}
@@ -153,67 +152,67 @@ table! {
table! {
post_authors (id) {
id -> Integer,
post_id -> Integer,
author_id -> Integer,
id -> Int4,
post_id -> Int4,
author_id -> Int4,
}
}
table! {
posts (id) {
id -> Integer,
blog_id -> Integer,
slug -> Text,
title -> Text,
id -> Int4,
blog_id -> Int4,
slug -> Varchar,
title -> Varchar,
content -> Text,
published -> Bool,
license -> Text,
license -> Varchar,
creation_date -> Timestamp,
ap_url -> Text,
ap_url -> Varchar,
subtitle -> Text,
source -> Text,
cover_id -> Nullable<Integer>,
cover_id -> Nullable<Int4>,
}
}
table! {
reshares (id) {
id -> Integer,
user_id -> Integer,
post_id -> Integer,
ap_url -> Text,
id -> Int4,
user_id -> Int4,
post_id -> Int4,
ap_url -> Varchar,
creation_date -> Timestamp,
}
}
table! {
tags (id) {
id -> Integer,
id -> Int4,
tag -> Text,
is_hashtag -> Bool,
post_id -> Integer,
post_id -> Int4,
}
}
table! {
users (id) {
id -> Integer,
username -> Text,
display_name -> Text,
outbox_url -> Text,
inbox_url -> Text,
id -> Int4,
username -> Varchar,
display_name -> Varchar,
outbox_url -> Varchar,
inbox_url -> Varchar,
is_admin -> Bool,
summary -> Text,
email -> Nullable<Text>,
hashed_password -> Nullable<Text>,
instance_id -> Integer,
instance_id -> Int4,
creation_date -> Timestamp,
ap_url -> Text,
private_key -> Nullable<Text>,
public_key -> Text,
shared_inbox_url -> Nullable<Text>,
followers_endpoint -> Text,
avatar_id -> Nullable<Integer>,
shared_inbox_url -> Nullable<Varchar>,
followers_endpoint -> Varchar,
avatar_id -> Nullable<Int4>,
last_fetched_date -> Timestamp,
fqn -> Text,
summary_html -> Text,
@@ -249,8 +248,8 @@ allow_tables_to_appear_in_same_query!(
apps,
blog_authors,
blogs,
comment_seers,
comments,
comment_seers,
follows,
instances,
likes,
+42 -10
View File
@@ -41,6 +41,7 @@ use blogs::Blog;
use db_conn::DbConn;
use follows::Follow;
use instance::*;
use ldap::Ldap;
use medias::Media;
use post_authors::PostAuthor;
use posts::Post;
@@ -333,17 +334,41 @@ impl User {
})
}
pub fn hash_pass(pass: &str) -> Result<String> {
fn hash_pass(pass: &str) -> Result<String> {
bcrypt::hash(pass, 10).map_err(Error::from)
}
pub fn auth(&self, pass: &str) -> bool {
fn auth(&self, pass: &str) -> bool {
self.hashed_password
.clone()
.map(|hashed| bcrypt::verify(pass, hashed.as_ref()).unwrap_or(false))
.unwrap_or(false)
}
pub fn connect(rocket: &PlumeRocket, name: &str, password: &str) -> Result<Self> {
let user = User::find_by_email(&*rocket.conn, &name)
.or_else(|_| User::find_by_fqn(&rocket, &name));
match user {
Ok(user) => {
let ldap_conn = Ldap::get_shared().connect(name.to_owned(), password.to_owned());
let local_conn = user.auth(password);
let ldap_conn = ldap_conn.get().unwrap_or(false);
if ldap_conn && local_conn {
user.clear_password(&rocket.conn).ok();
}
if ldap_conn || local_conn {
Ok(user)
} else {
Err(Error::NotFound)
}
}
Err(_) => {
User::get(&rocket.conn, 1)?.auth(password);
Err(Error::NotFound)
}
}
}
pub fn reset_password(&self, conn: &Connection, pass: &str) -> Result<()> {
diesel::update(self)
.set(users::hashed_password.eq(User::hash_pass(pass)?))
@@ -351,6 +376,13 @@ impl User {
Ok(())
}
fn clear_password(&self, conn: &Connection) -> Result<()> {
diesel::update(self)
.set(users::hashed_password.eq::<Option<String>>(None))
.execute(conn)?;
Ok(())
}
pub fn get_local_page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<User>> {
users::table
.filter(users::instance_id.eq(Instance::get_local()?.id))
@@ -707,7 +739,7 @@ impl User {
mime_type: None,
href: None,
template: Some(format!(
"https://{}/remote_interact?{{uri}}",
"{}/remote_interact?{{uri}}",
self.get_instance(conn)?.public_domain
)),
},
@@ -923,7 +955,7 @@ impl NewUser {
is_admin: bool,
summary: &str,
email: String,
password: String,
password: &str,
) -> Result<User> {
let (pub_key, priv_key) = gen_keypair();
User::insert(
@@ -935,7 +967,7 @@ impl NewUser {
summary: summary.to_owned(),
summary_html: SafeString::new(&utils::md_to_html(&summary, None, false, None).0),
email: Some(email),
hashed_password: Some(password),
hashed_password: Some(User::hash_pass(password)?),
instance_id: Instance::get_local()?.id,
ap_url: String::new(),
public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?,
@@ -964,7 +996,7 @@ pub(crate) mod tests {
true,
"Hello there, I'm the admin",
"admin@example.com".to_owned(),
"invalid_admin_password".to_owned(),
"invalid_admin_password",
)
.unwrap();
let user = NewUser::new_local(
@@ -974,7 +1006,7 @@ pub(crate) mod tests {
false,
"Hello there, I'm no one",
"user@example.com".to_owned(),
"invalid_user_password".to_owned(),
"invalid_user_password",
)
.unwrap();
let other = NewUser::new_local(
@@ -984,7 +1016,7 @@ pub(crate) mod tests {
false,
"Hello there, I'm someone else",
"other@example.com".to_owned(),
"invalid_other_password".to_owned(),
"invalid_other_password",
)
.unwrap();
vec![admin, user, other]
@@ -1003,7 +1035,7 @@ pub(crate) mod tests {
false,
"Hello I'm a test",
"test@example.com".to_owned(),
User::hash_pass("test_password").unwrap(),
"test_password",
)
.unwrap();
@@ -1109,7 +1141,7 @@ pub(crate) mod tests {
false,
"Hello I'm a test",
"test@example.com".to_owned(),
User::hash_pass("test_password").unwrap(),
"test_password",
)
.unwrap();
+9 -9
View File
@@ -36,39 +36,39 @@ msgstr ""
msgid "{0}'s avatar"
msgstr ""
# src/routes/blogs.rs:96
# src/routes/blogs.rs:64
msgid "To create a new blog, you need to be logged in"
msgstr ""
# src/routes/blogs.rs:138
# src/routes/blogs.rs:106
msgid "A blog with the same name already exists."
msgstr ""
# src/routes/blogs.rs:173
# src/routes/blogs.rs:141
msgid "Your blog was successfully created!"
msgstr ""
# src/routes/blogs.rs:195
# src/routes/blogs.rs:163
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:202
# src/routes/blogs.rs:170
msgid "You are not allowed to delete this blog."
msgstr ""
# src/routes/blogs.rs:250
# src/routes/blogs.rs:218
msgid "You are not allowed to edit this blog."
msgstr ""
# src/routes/blogs.rs:295
# src/routes/blogs.rs:263
msgid "You can't use this media as a blog icon."
msgstr ""
# src/routes/blogs.rs:313
# src/routes/blogs.rs:281
msgid "You can't use this media as a blog banner."
msgstr ""
# src/routes/blogs.rs:346
# src/routes/blogs.rs:314
msgid "Your blog information have been updated."
msgstr ""
+13 -23
View File
@@ -62,30 +62,20 @@ pub fn oauth(
let conn = &*rockets.conn;
let app = App::find_by_client_id(conn, &query.client_id)?;
if app.client_secret == query.client_secret {
if let Ok(user) = User::find_by_fqn(&rockets, &query.username) {
if user.auth(&query.password) {
let token = ApiToken::insert(
conn,
NewApiToken {
app_id: app.id,
user_id: user.id,
value: random_hex(),
scopes: query.scopes.clone(),
},
)?;
Ok(Json(json!({
"token": token.value
})))
} else {
Ok(Json(json!({
"error": "Invalid credentials"
})))
}
if let Ok(user) = User::connect(&rockets, &query.username, &query.password) {
let token = ApiToken::insert(
conn,
NewApiToken {
app_id: app.id,
user_id: user.id,
value: random_hex(),
scopes: query.scopes.clone(),
},
)?;
Ok(Json(json!({
"token": token.value
})))
} else {
// Making fake password verification to avoid different
// response times that would make it possible to know
// if a username is registered or not.
User::get(conn, 1)?.auth(&query.password);
Ok(Json(json!({
"error": "Invalid credentials"
})))
+4 -45
View File
@@ -22,7 +22,6 @@ extern crate num_cpus;
extern crate plume_api;
extern crate plume_common;
extern crate plume_models;
extern crate reqwest;
#[macro_use]
extern crate rocket;
extern crate rocket_contrib;
@@ -43,21 +42,18 @@ extern crate webfinger;
use clap::App;
use diesel::r2d2::ConnectionManager;
use plume_models::{
blogs::Blog,
blogs::Host,
db_conn::{DbPool, PragmaForeignKey},
instance::Instance,
ldap::Ldap,
migrations::IMPORTED_MIGRATIONS,
search::{Searcher as UnmanagedSearcher, SearcherError},
Connection, Error, CONFIG,
};
use rocket::{fairing::AdHoc, http::ext::IntoOwned, http::uri::Origin};
use rocket_csrf::CsrfFairingBuilder;
use scheduled_thread_pool::ScheduledThreadPool;
use std::collections::HashMap;
use std::process::exit;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use std::time::Duration;
init_i18n!(
"plume", ar, bg, ca, cs, de, en, eo, es, fr, gl, hi, hr, it, ja, nb, pl, pt, ro, ru, sr, sk, sv
@@ -86,13 +82,13 @@ fn init_pool() -> Option<DbPool> {
e => e.map(|_| ()).unwrap(),
}
let _ = Ldap::get_shared(); // force initialization of lazy static
let manager = ConnectionManager::<Connection>::new(CONFIG.database_url.as_str());
let pool = DbPool::builder()
.connection_customizer(Box::new(PragmaForeignKey))
.build(manager)
.ok()?;
Instance::cache_local(&pool.get().unwrap());
Blog::cache_custom_domains(&pool.get().unwrap());
Some(pool)
}
@@ -181,42 +177,7 @@ Then try to restart Plume
println!("Please refer to the documentation to see how to configure it.");
}
let custom_domain_fairing = AdHoc::on_request("Custom Blog Domains", |req, _data| {
let host = req.guard::<Host>();
if host.is_success()
&& req
.uri()
.segments()
.next()
.map(|path| path != "static" && path != "api")
.unwrap_or(true)
{
let rewrite_uri = format!("/custom_domains/{}/{}", host.unwrap(), req.uri());
let uri = Origin::parse_owned(rewrite_uri).unwrap();
let uri = uri.to_normalized().into_owned();
req.set_uri(uri);
}
});
let valid_domains: HashMap<String, Instant> = HashMap::new();
let rocket = rocket::custom(CONFIG.rocket.clone().unwrap())
.mount(
"/custom_domains/domain_validation/",
routes![routes::blogs::custom::domain_validation,],
)
.mount(
"/domain_validation/",
routes![routes::blogs::domain_validation,],
)
.mount(
"/custom_domains/",
routes![
routes::blogs::custom::details,
routes::posts::custom::details,
routes::blogs::custom::activity_details,
routes::search::custom::search,
],
)
.mount(
"/",
routes![
@@ -329,7 +290,6 @@ Then try to restart Plume
.manage(dbpool)
.manage(Arc::new(workpool))
.manage(searcher)
.manage(Mutex::new(valid_domains))
.manage(include_i18n!())
.attach(
CsrfFairingBuilder::new()
@@ -356,8 +316,7 @@ Then try to restart Plume
])
.finalize()
.expect("main: csrf fairing creation error"),
)
.attach(custom_domain_fairing);
);
#[cfg(feature = "test")]
let rocket = rocket.mount("/test", routes![test_routes::health,]);
+12 -174
View File
@@ -2,14 +2,11 @@ use activitypub::collection::OrderedCollection;
use atom_syndication::{Entry, FeedBuilder};
use diesel::SaveChangesDsl;
use rocket::{
http::{ContentType, Status},
http::ContentType,
request::LenientForm,
response::{content::Content, Flash, Redirect},
State,
};
use rocket_i18n::I18n;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use std::{borrow::Cow, collections::HashMap};
use validator::{Validate, ValidationError, ValidationErrors};
@@ -19,17 +16,14 @@ use plume_models::{
blog_authors::*, blogs::*, instance::Instance, medias::*, posts::Post, safe_string::SafeString,
users::User, Connection, PlumeRocket,
};
use reqwest::Client;
use routes::{errors::ErrorPage, Page, RespondOrRedirect};
use template_utils::{IntoContext, Ructe};
fn detail_guts(
blog: Blog,
page: Option<Page>,
rockets: PlumeRocket,
) -> Result<RespondOrRedirect, ErrorPage> {
#[get("/~/<name>?<page>", rank = 2)]
pub fn details(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default();
let conn = &*rockets.conn;
let blog = Blog::find_by_fqn(&rockets, &name)?;
let posts = Post::blog_page(conn, &blog, page.limits())?;
let articles_count = Post::count_for_blog(conn, &blog)?;
let authors = &blog.list_authors(conn)?;
@@ -41,43 +35,7 @@ fn detail_guts(
page.0,
Page::total(articles_count as i32),
posts
))
.into())
}
#[get("/~/<name>?<page>", rank = 2)]
pub fn details(
name: String,
page: Option<Page>,
rockets: PlumeRocket,
) -> Result<RespondOrRedirect, ErrorPage> {
let blog = Blog::find_by_fqn(&rockets, &name)?;
// check this first, and return early
// doing this prevents partially moving `blog` into the `match (tuple)`,
// which makes it impossible to reuse then.
if blog.custom_domain == None {
return detail_guts(blog, page, rockets);
}
match (blog.custom_domain, page) {
(Some(ref custom_domain), Some(ref page)) => {
Ok(Redirect::to(format!("https://{}/?page={}", custom_domain, page)).into())
}
(Some(ref custom_domain), _) => {
Ok(Redirect::to(format!("https://{}/", custom_domain)).into())
}
// we need this match arm, or the match won't compile
(None, _) => unreachable!("This code path should have already been handled!"),
}
}
pub fn activity_detail_guts(
blog: Blog,
rockets: PlumeRocket,
_ap: ApRequest,
) -> Option<ActivityStream<CustomGroup>> {
Some(ActivityStream::new(blog.to_activity(&*rockets.conn).ok()?))
)))
}
#[get("/~/<name>", rank = 1)]
@@ -87,7 +45,7 @@ pub fn activity_details(
_ap: ApRequest,
) -> Option<ActivityStream<CustomGroup>> {
let blog = Blog::find_by_fqn(&rockets, &name).ok()?;
activity_detail_guts(blog, rockets, _ap)
Some(ActivityStream::new(blog.to_activity(&*rockets.conn).ok()?))
}
#[get("/blogs/new")]
@@ -99,76 +57,6 @@ pub fn new(rockets: PlumeRocket, _user: User) -> Ructe {
))
}
// mounted as /domain_validation/
#[get("/<validation_id>")]
pub fn domain_validation(
validation_id: String,
valid_domains: State<Mutex<HashMap<String, Instant>>>,
) -> Status {
let mutex = valid_domains.inner().lock();
let mut validation_map = mutex.unwrap();
let validation_getter = validation_map.clone();
let value = validation_getter.get(&validation_id);
if value.is_none() {
// validation id not found
return Status::NotFound;
}
// we have valid id, now check the time
let valid_until = value.unwrap();
let now = Instant::now();
// nope, expired (410: gone)
if now.duration_since(*valid_until).as_secs() > 0 {
validation_map.remove(&validation_id);
// validation expired
return Status::Gone;
}
validation_map.remove(&validation_id);
Status::Ok
}
pub mod custom {
use plume_common::activity_pub::{ActivityStream, ApRequest};
use plume_models::{blogs::Blog, blogs::CustomGroup, blogs::Host, PlumeRocket};
use rocket::{http::Status, State};
use routes::{errors::ErrorPage, Page, RespondOrRedirect};
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::Instant;
#[get("/<custom_domain>?<page>", rank = 2)]
pub fn details(
custom_domain: String,
page: Option<Page>,
rockets: PlumeRocket,
) -> Result<RespondOrRedirect, ErrorPage> {
let blog = Blog::find_by_host(&rockets, Host::new(custom_domain))?;
super::detail_guts(blog, page, rockets)
}
#[get("/<custom_domain>", rank = 1)]
pub fn activity_details(
custom_domain: String,
rockets: PlumeRocket,
_ap: ApRequest,
) -> Option<ActivityStream<CustomGroup>> {
let blog = Blog::find_by_host(&rockets, Host::new(custom_domain)).ok()?;
super::activity_detail_guts(blog, rockets, _ap)
}
// mounted as /custom_domains/domain_validation/
#[get("/<validation_id>")]
pub fn domain_validation(
validation_id: String,
valid_domains: State<Mutex<HashMap<String, Instant>>>,
) -> Status {
super::domain_validation(validation_id, valid_domains)
}
}
#[get("/blogs/new", rank = 2)]
pub fn new_auth(i18n: I18n) -> Flash<Redirect> {
utils::requires_login(
@@ -184,7 +72,6 @@ pub fn new_auth(i18n: I18n) -> Flash<Redirect> {
pub struct NewBlogForm {
#[validate(custom(function = "valid_slug", message = "Invalid name"))]
pub title: String,
pub custom_domain: String,
}
fn valid_slug(title: &str) -> Result<(), ValidationError> {
@@ -196,43 +83,13 @@ fn valid_slug(title: &str) -> Result<(), ValidationError> {
}
}
fn valid_domain(domain: &str, valid_domains: State<Mutex<HashMap<String, Instant>>>) -> bool {
let mutex = valid_domains.inner().lock();
let mut validation_map = mutex.unwrap();
let random_id = utils::random_hex();
validation_map.insert(
random_id.clone(),
Instant::now().checked_add(Duration::new(60, 0)).unwrap(),
);
let client = Client::new();
let validation_uri = format!("https://{}/domain_validation/{}", domain, random_id);
match client.get(&validation_uri).send() {
Ok(resp) => resp.status().is_success(),
Err(_) => false,
}
}
#[post("/blogs/new", data = "<form>")]
pub fn create(
form: LenientForm<NewBlogForm>,
rockets: PlumeRocket,
valid_domains: State<Mutex<HashMap<String, Instant>>>,
) -> RespondOrRedirect {
pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> RespondOrRedirect {
let slug = utils::make_actor_id(&form.title);
let conn = &*rockets.conn;
let intl = &rockets.intl.catalog;
let user = rockets.user.clone().unwrap();
let (custom_domain, dns_ok) = if form.custom_domain.is_empty() {
(None, true)
} else {
let dns_check = valid_domain(&form.custom_domain.clone(), valid_domains);
(Some(Host::new(form.custom_domain.clone())), dns_check)
};
let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(),
Err(e) => e,
@@ -264,7 +121,6 @@ pub fn create(
Instance::get_local()
.expect("blog::create: instance error")
.id,
custom_domain,
)
.expect("blog::create: new local error"),
)
@@ -280,19 +136,11 @@ pub fn create(
)
.expect("blog::create: author error");
if dns_ok {
Flash::success(
Redirect::to(uri!(details: name = slug.clone(), page = _)),
&i18n!(intl, "Your blog was successfully created!"),
)
.into()
} else {
Flash::warning(
Redirect::to(uri!(details: name = slug.clone(), page = _)),
&i18n!(intl, "Your blog was successfully created, but the custom domain seems invalid. Please check it is correct from your blog's settings."),
)
.into()
}
Flash::success(
Redirect::to(uri!(details: name = slug.clone(), page = _)),
&i18n!(intl, "Your blog was successfully created!"),
)
.into()
}
#[post("/~/<name>/delete")]
@@ -333,7 +181,6 @@ pub struct EditForm {
pub summary: String,
pub icon: Option<i32>,
pub banner: Option<i32>,
pub custom_domain: String,
}
#[get("/~/<name>/edit")]
@@ -351,10 +198,6 @@ pub fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
.clone()
.expect("blogs::edit: User was None while it shouldn't");
let medias = Media::for_user(conn, user.id).expect("Couldn't list media");
let custom_domain = match blog.custom_domain {
Some(ref c) => c.to_string(),
_ => String::from(""),
};
Ok(render!(blogs::edit(
&rockets.to_context(),
&blog,
@@ -364,7 +207,6 @@ pub fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
summary: blog.summary.clone(),
icon: blog.icon_id,
banner: blog.banner_id,
custom_domain: custom_domain,
},
ValidationErrors::default()
)))
@@ -476,10 +318,6 @@ pub fn update(
);
blog.icon_id = form.icon;
blog.banner_id = form.banner;
if !form.custom_domain.is_empty() {
blog.custom_domain = Some(Host::new(form.custom_domain.clone()))
}
blog.save_changes::<Blog>(&*conn)
.expect("Couldn't save blog changes");
Ok(Flash::success(
+4 -21
View File
@@ -1,6 +1,6 @@
use plume_models::{instance::Instance, Error, PlumeRocket};
use plume_models::{Error, PlumeRocket};
use rocket::{
response::{self, Redirect, Responder},
response::{self, Responder},
Request,
};
use template_utils::{IntoContext, Ructe};
@@ -29,26 +29,9 @@ impl<'r> Responder<'r> for ErrorPage {
}
#[catch(404)]
pub fn not_found(req: &Request) -> Result<Ructe, Redirect> {
pub fn not_found(req: &Request) -> Ructe {
let rockets = req.guard::<PlumeRocket>().unwrap();
if req
.uri()
.segments()
.next()
.map(|path| path == "custom_domains")
.unwrap_or(false)
{
let path = req
.uri()
.segments()
.skip(2)
.collect::<Vec<&str>>()
.join("/");
let public_domain = Instance::get_local().unwrap().public_domain;
Err(Redirect::to(format!("https://{}/{}", public_domain, path)))
} else {
Ok(render!(errors::not_found(&rockets.to_context())))
}
render!(errors::not_found(&rockets.to_context()))
}
#[catch(422)]
+1 -8
View File
@@ -10,7 +10,6 @@ use rocket::{
response::{Flash, NamedFile, Redirect},
Outcome,
};
use std::fmt;
use std::path::{Path, PathBuf};
use template_utils::Ructe;
@@ -53,15 +52,9 @@ impl From<Flash<Redirect>> for RespondOrRedirect {
}
}
#[derive(Debug, Shrinkwrap, Copy, Clone, UriDisplayQuery)]
#[derive(Shrinkwrap, Copy, Clone, UriDisplayQuery)]
pub struct Page(i32);
impl fmt::Display for Page {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<'v> FromFormValue<'v> for Page {
type Error = &'v RawStr;
fn from_form_value(form_value: &'v RawStr) -> Result<Page, &'v RawStr> {
+21 -85
View File
@@ -31,14 +31,28 @@ use routes::{
};
use template_utils::{IntoContext, Ructe};
fn detail_guts(
blog: &Blog,
post: &Post,
#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
pub fn details(
blog: String,
slug: String,
responding_to: Option<i32>,
rockets: &PlumeRocket,
) -> Result<RespondOrRedirect, ErrorPage> {
rockets: PlumeRocket,
) -> Result<Ructe, ErrorPage> {
let conn = &*rockets.conn;
let user = rockets.user.clone();
let blog = Blog::find_by_fqn(&rockets, &blog)?;
let post = Post::find_by_slug(&*conn, &slug, blog.id)?;
if !(post.published
|| post
.get_authors(&*conn)?
.into_iter()
.any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)))
{
return Ok(render!(errors::not_authorized(
&rockets.to_context(),
i18n!(rockets.intl.catalog, "This post isn't published yet.")
)));
}
let comments = CommentTree::from_post(&*conn, &post, user.as_ref())?;
@@ -47,7 +61,7 @@ fn detail_guts(
Ok(render!(posts::details(
&rockets.to_context(),
post.clone(),
blog.clone(),
blog,
&NewCommentForm {
warning: previous.clone().map(|p| p.spoiler_text).unwrap_or_default(),
content: previous.clone().and_then(|p| Some(format!(
@@ -80,85 +94,7 @@ fn detail_guts(
user.clone().and_then(|u| u.has_reshared(&*conn, &post).ok()).unwrap_or(false),
user.and_then(|u| u.is_following(&*conn, post.get_authors(&*conn).ok()?[0].id).ok()).unwrap_or(false),
post.get_authors(&*conn)?[0].clone()
)).into())
}
#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
pub fn details(
blog: String,
slug: String,
responding_to: Option<i32>,
rockets: PlumeRocket,
) -> Result<RespondOrRedirect, ErrorPage> {
let conn = &*rockets.conn;
let user = rockets.user.clone();
let blog = Blog::find_by_fqn(&rockets, &blog)?;
let post = Post::find_by_slug(&*conn, &slug, blog.id)?;
if !(post.published
|| post
.get_authors(&*conn)?
.into_iter()
.any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)))
{
return Ok(render!(errors::not_authorized(
&rockets.to_context(),
i18n!(rockets.intl.catalog, "This post isn't published yet.")
))
.into());
}
// check this first, and return early
// doing this prevents partially moving `blog` into the `match (tuple)`,
// which makes it impossible to reuse then.
if blog.custom_domain == None {
return detail_guts(&blog, &post, responding_to, &rockets);
}
match (blog.custom_domain, responding_to) {
(Some(ref custom_domain), Some(ref responding_to)) => Ok(Redirect::to(format!(
"https://{}/{}?responding_to={}",
custom_domain, slug, responding_to
))
.into()),
(Some(ref custom_domain), _) => {
Ok(Redirect::to(format!("https://{}/{}", custom_domain, slug)).into())
}
(None, _) => unreachable!("This code path should have already been handled!"),
}
}
pub mod custom {
use plume_models::{blogs::Blog, blogs::Host, posts::Post, PlumeRocket};
use routes::{errors::ErrorPage, RespondOrRedirect};
use template_utils::{IntoContext, Ructe};
#[get("/<custom_domain>/<slug>?<responding_to>", rank = 4)]
pub fn details(
custom_domain: String,
slug: String,
responding_to: Option<i32>,
rockets: PlumeRocket,
) -> Result<RespondOrRedirect, ErrorPage> {
let conn = &*rockets.conn;
let user = rockets.user.clone();
let blog = Blog::find_by_host(&rockets, Host::new(custom_domain))?;
let post = Post::find_by_slug(&*conn, &slug, blog.id)?;
if !(post.published
|| post
.get_authors(&*conn)?
.into_iter()
.any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)))
{
return Ok(render!(errors::not_authorized(
&rockets.to_context(),
i18n!(rockets.intl.catalog, "This post isn't published yet.")
))
.into());
}
super::detail_guts(&blog, &post, responding_to, &rockets)
}
)))
}
#[get("/~/<blog>/<slug>", rank = 3)]
+2 -21
View File
@@ -49,7 +49,8 @@ macro_rules! param_to_query {
}
}
fn search_guts(query: Option<Form<SearchQuery>>, rockets: PlumeRocket) -> Ructe {
#[get("/search?<query..>")]
pub fn search(query: Option<Form<SearchQuery>>, rockets: PlumeRocket) -> Ructe {
let conn = &*rockets.conn;
let query = query.map(Form::into_inner).unwrap_or_default();
let page = query.page.unwrap_or_default();
@@ -82,23 +83,3 @@ fn search_guts(query: Option<Form<SearchQuery>>, rockets: PlumeRocket) -> Ructe
))
}
}
#[get("/search?<query..>")]
pub fn search(query: Option<Form<SearchQuery>>, rockets: PlumeRocket) -> Ructe {
search_guts(query, rockets)
}
pub mod custom {
use plume_models::PlumeRocket;
use rocket::request::Form;
use template_utils::Ructe;
#[get("/<_custom_domain>/search?<query..>")]
pub fn search(
_custom_domain: String,
query: Option<Form<super::SearchQuery>>,
rockets: PlumeRocket,
) -> Ructe {
super::search_guts(query, rockets)
}
}
+7 -31
View File
@@ -33,53 +33,29 @@ pub fn new(m: Option<String>, rockets: PlumeRocket) -> Ructe {
))
}
#[derive(Default, FromForm, Validate)]
#[derive(Default, FromForm)]
pub struct LoginForm {
#[validate(length(min = "1", message = "We need an email, or a username to identify you"))]
pub email_or_name: String,
#[validate(length(min = "1", message = "Your password can't be empty"))]
pub password: String,
}
#[post("/login", data = "<form>")]
pub fn create(
form: LenientForm<LoginForm>,
mut cookies: Cookies,
rockets: PlumeRocket,
mut cookies: Cookies,
) -> RespondOrRedirect {
let conn = &*rockets.conn;
let user = User::find_by_email(&*conn, &form.email_or_name)
.or_else(|_| User::find_by_fqn(&rockets, &form.email_or_name));
let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(),
Err(e) => e,
};
let user_id = if let Ok(user) = user {
if !user.auth(&form.password) {
let mut err = ValidationError::new("invalid_login");
err.message = Some(Cow::from("Invalid username, or password"));
errors.add("email_or_name", err);
String::new()
} else {
user.id.to_string()
}
let user_id = if let Ok(user) = User::connect(&rockets, &form.email_or_name, &form.password) {
user.id.to_string()
} else {
// Fake password verification, only to avoid different login times
// that could be used to see if an email adress is registered or not
User::get(&*conn, 1)
.map(|u| u.auth(&form.password))
.expect("No user is registered");
let mut errors = ValidationErrors::new();
let mut err = ValidationError::new("invalid_login");
err.message = Some(Cow::from("Invalid username, or password"));
errors.add("email_or_name", err);
String::new()
};
if !errors.is_empty() {
return render!(session::login(&rockets.to_context(), None, &*form, errors)).into();
}
};
cookies.add_private(
Cookie::build(AUTH_COOKIE, user_id)
@@ -111,7 +87,7 @@ pub fn create(
&(conn, &rockets.intl.catalog, None, None),
None,
&*form,
errors
ValidationErrors::new()
))
.into()
}
+1 -1
View File
@@ -520,7 +520,7 @@ pub fn create(
false,
"",
form.email.to_string(),
User::hash_pass(&form.password).map_err(to_validation)?,
&form.password,
)
.map_err(to_validation)?;
Ok(Flash::success(
+11 -11
View File
@@ -48,16 +48,14 @@ impl Resolver<PlumeRocket> for WebfingerResolver {
CONFIG.base_url.as_str()
}
fn find(prefix: Prefix, acct: String, ctx: PlumeRocket) -> Result<Webfinger, ResolverError> {
match prefix {
Prefix::Acct => User::find_by_fqn(&ctx, &acct)
.and_then(|usr| usr.webfinger(&*ctx.conn))
.or(Err(ResolverError::NotFound)),
Prefix::Group => Blog::find_by_fqn(&ctx, &acct)
.and_then(|blog| blog.webfinger(&*ctx.conn))
.or(Err(ResolverError::NotFound)),
Prefix::Custom(_) => Err(ResolverError::NotFound),
}
fn find(acct: String, ctx: PlumeRocket) -> Result<Webfinger, ResolverError> {
User::find_by_fqn(&ctx, &acct)
.and_then(|usr| usr.webfinger(&*ctx.conn))
.or_else(|_| {
Blog::find_by_fqn(&ctx, &acct)
.and_then(|blog| blog.webfinger(&*ctx.conn))
.or(Err(ResolverError::NotFound))
})
}
}
@@ -74,7 +72,9 @@ pub fn webfinger(resource: String, rockets: PlumeRocket) -> Content<String> {
"Invalid resource. Make sure to request an acct: URI"
}
ResolverError::NotFound => "Requested resource was not found",
ResolverError::WrongDomain => "This is not the instance of the requested resource",
ResolverError::WrongInstance => {
"This is not the instance of the requested resource"
}
}),
),
}
+1 -101
View File
@@ -1,10 +1,7 @@
use plume_models::{notifications::*, users::User, Connection, PlumeRocket};
use rocket::http::hyper::header::{ETag, EntityTag};
use rocket::http::{
uri::{FromUriParam, Query},
Method, Status,
};
use rocket::http::{Method, Status};
use rocket::request::Request;
use rocket::response::{self, content::Html as HtmlCt, Responder, Response};
use rocket_i18n::Catalog;
@@ -16,16 +13,6 @@ pub use askama_escape::escape;
pub static CACHE_NAME: &str = env!("CACHE_ID");
pub struct NoValue; // workarround for missing FromUriParam implementation for Option
impl FromUriParam<Query, NoValue> for Option<i32> {
type Target = Option<i32>;
fn from_uri_param(_: NoValue) -> Self::Target {
None
}
}
pub type BaseContext<'a> = &'a (
&'a Connection,
&'a Catalog,
@@ -355,90 +342,3 @@ macro_rules! input {
))
}};
}
/// This macro imitate rocket's uri!, but with support for custom domains
///
/// It takes one more argument, domain, which must appear first, and must be an Option<&str>
/// sample call :
/// assuming both take the same parameters
/// url!(custom_domain=Some("something.tld"), posts::details: slug = "title", responding_to = _, blog = "blogname"));
///
/// assuming posts::details take one more parameter than posts::custom::details
/// url!(custom_domain=Some("something.tld"), posts::details:
/// common=[slug = "title", responding_to = _],
/// normal=[blog = "blogname"]));
///
/// you can also provide custom=[] for custom-domain specific arguments
/// custom_domain can be changed to anything, indicating custom domain varname in the custom-domain
/// function (most likely custom_domain or _custom_domain)
macro_rules! url {
($custom_domain:ident=$domain:expr, $module:ident::$route:ident:
common=[$($common_args:tt = $common_val:expr),*],
normal=[$($normal_args:tt = $normal_val:expr),*],
custom=[$($custom_args:tt = $custom_val:expr),*]) => {{
let domain: &Option<plume_models::blogs::Host> = &$domain; //for type inference with None
$(
let $common_args = $common_val;
)*
if let Some(domain) = domain {
$(
let $custom_args = $custom_val;
)*
let origin = uri!(crate::routes::$module::custom::$route:
$custom_domain = domain.to_string(),
$($common_args = $common_args,)*
$($custom_args = $custom_args,)*
);
let path = origin
.segments()
.skip(1)// skip is <custom_domain> part
.map(|seg| format!("/{}", seg)).collect::<String>();
let query = origin.query()
.filter(|q| !q.is_empty())
.map(|q| format!("?{}", q))
.unwrap_or_default();
format!("https://{}{}{}", &domain, path, query)
} else {
$(
let $normal_args = $normal_val;
)*
url!($module::$route:
$($common_args = $common_args,)*
$($normal_args = $normal_args,)*)
.to_string()
}
}};
($cd:ident=$d:expr, $m:ident::$r:ident:
common=[$($tt:tt)*]) => {
url!($cd=$d, $m::$r: common=[$($tt)*], normal=[], custom=[])
};
($cd:ident=$d:expr, $m:ident::$r:ident:
normal=[$($tt:tt)*]) => {
url!($cd=$d, $m::$r: common=[], normal=[$($tt)*], custom=[])
};
($cd:ident=$d:expr, $m:ident::$r:ident:
custom=[$($tt:tt)*]) => {
url!($cd=$d, $m::$r: common=[], normal=[], custom=[$($tt)*])
};
($cd:ident=$d:expr, $m:ident::$r:ident:
common=[$($co:tt)*],
normal=[$($no:tt)*]) => {
url!($cd=$d, $m::$r: common=[$($co)*], normal=[$($no)*], custom=[])
};
($cd:ident=$d:expr, $m:ident::$r:ident:
common=[$($co:tt)*],
custom=[$($cu:tt)*]) => {
url!($cd=$d, $m::$r: common=[$($co)*], normal=[], custom=[$($cu)*])
};
($cd:ident=$d:expr, $m:ident::$r:ident:
normal=[$($no:tt)*],
custom=[$($cu:tt)*]) => {
url!($cd=$d, $m::$r: common=[], normal=[$($no)*], custom=[$($cu)*])
};
($custom_domain:ident=$domain:expr, $module:ident::$route:ident: $($common_args:tt)*) => {
url!($custom_domain=$domain, $module::$route: common=[$($common_args)*])
};
($module:ident::$route:ident: $($tt:tt)*) => {
uri!(crate::routes::$module::$route: $($tt)*)
};
}
+4 -4
View File
@@ -13,8 +13,8 @@
<meta content="120" property="og:image:width" />
<meta content="120" property="og:image:height" />
<meta content="summary" property="twitter:card" />
<meta content="@Instance::get_local().unwrap().name" property="og:site_name" />
<meta content="@blog.url()" property="og:url" />
<meta content="'@Instance::get_local().unwrap().name" property="og:site_name" />
<meta content="@blog.ap_url" property="og:url" />
<meta content="@blog.fqn" property="profile:username" />
<meta content="@blog.title" property="og:title" />
<meta content="@blog.summary_html" name="description">
@@ -24,7 +24,7 @@
<link href='@Instance::get_local().unwrap().compute_box("~", &blog.fqn, "atom.xml")' rel='alternate' type='application/atom+xml'>
<link href='@blog.ap_url' rel='alternate' type='application/activity+json'>
}, {
<a href="@url!(custom_domain = blog.custom_domain, blogs::details: common=[page = None], normal=[name = &blog.fqn])" dir="auto">@blog.title</a>
<a href="@uri!(blogs::details: name = &blog.fqn, page = _)" dir="auto">@blog.title</a>
}, {
<div class="hidden">
@for author in authors {
@@ -55,7 +55,7 @@
<a href="@uri!(blogs::edit: name = &blog.fqn)" class="button" dir="auto">@i18n!(ctx.1, "Edit")</a>
}
</div>
<main class="user-summary" dir="auto">
<p>
@i18n!(ctx.1, "There's one author on this blog: ", "There are {0} authors on this blog: "; authors.len())
-2
View File
@@ -23,8 +23,6 @@
<label for="summary">@i18n!(ctx.1, "Description")<small>@i18n!(ctx.1, "Markdown syntax is supported")</small></label>
<textarea id="summary" name="summary" rows="20">@form.summary</textarea>
@input!(ctx.1, custom_domain (optional text), "Custom Domain", form, errors, "")
<p>
@i18n!(ctx.1, "You can upload images to your gallery, to use them as blog icons, or banners.")
<a href="@uri!(medias::new)">@i18n!(ctx.1, "Upload images")</a>
+1 -4
View File
@@ -9,10 +9,7 @@
@:base(ctx, i18n!(ctx.1, "New Blog"), {}, {}, {
<h1 dir="auto">@i18n!(ctx.1, "Create a blog")</h1>
<form method="post" action="@uri!(blogs::create)">
@input!(ctx.1, title (text), "Title", form, errors.clone(), "required minlength=\"1\"")
@input!(ctx.1, title (text), "Title", form, errors, "required minlength=\"1\"")
<input type="submit" value="@i18n!(ctx.1, "Create blog")" dir="auto"/>
@input!(ctx.1, custom_domain (optional text), "Custom Domain", form, errors, "")
<input type="submit" value="@i18n!(ctx.1, "Make your blog available under a custom domain")" dir="auto"/>
</form>
})
+3 -2
View File
@@ -9,7 +9,7 @@
<div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div>
}
<h3 class="p-name" dir="auto">
<a class="u-url" href="@url!(custom_domain = article.get_blog(ctx.0).unwrap().custom_domain, posts::details: common=[ slug = &article.slug, responding_to = NoValue], normal=[blog = article.get_blog(ctx.0).unwrap().fqn])">
<a class="u-url" href="@uri!(posts::details: blog = article.get_blog(ctx.0).unwrap().fqn, slug = &article.slug, responding_to = _)">
@article.title
</a>
</h3>
@@ -25,9 +25,10 @@
@if article.published {
<span class="dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span>
}
<a href="@url!(custom_domain = article.get_blog(ctx.0).unwrap().custom_domain, blogs::details: common=[page = None], normal=[name = &article.get_blog(ctx.0).unwrap().fqn])">@article.get_blog(ctx.0).unwrap().title</a>
<a href="@uri!(blogs::details: name = &article.get_blog(ctx.0).unwrap().fqn, page = _)">@article.get_blog(ctx.0).unwrap().title</a>
@if !article.published {
⋅ @i18n!(ctx.1, "Draft")
}
</footer>
</div>
+3 -3
View File
@@ -17,10 +17,10 @@
@if article.cover_id.is_some() {
<meta property="og:image" content="@Html(article.cover_url(ctx.0).unwrap_or_default())"/>
}
<meta property="og:url" content="@url!(custom_domain = blog.custom_domain, posts::details: common=[slug = &article.slug, responding_to = NoValue], normal=[blog = &blog.fqn])"/>
<meta property="og:url" content="@uri!(posts::details: blog = &blog.fqn, slug = &article.slug, responding_to = _)"/>
<meta property="og:description" content="@article.subtitle"/>
}, {
<a href="@url!(custom_domain = &blog.custom_domain, blogs::details: common=[page = None], normal=[name = &blog.fqn])">@blog.title</a>
<a href="@uri!(blogs::details: name = &blog.fqn, page = _)">@blog.title</a>
}, {
<div class="h-entry">
<header
@@ -54,7 +54,7 @@
<ul class="tags" dir="auto">
@for tag in tags {
@if !tag.is_hashtag {
<li><a class="p-category" href="@uri!(tags::tag: name = &tag.tag, page = None)">@tag.tag</a></li>
<li><a class="p-category" href="@uri!(tags::tag: name = &tag.tag, page = _)">@tag.tag</a></li>
} else {
<span class="hidden p-category">@tag.tag</span>
}