mirror of
https://gitlab.com/famedly/conduit.git
synced 2025-06-27 16:35:59 +00:00
feat(media): blocking
This commit is contained in:
parent
d76637048a
commit
594fe5f98f
9 changed files with 738 additions and 19 deletions
69
Cargo.lock
generated
69
Cargo.lock
generated
|
@ -38,6 +38,21 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android-tzdata"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle"
|
name = "anstyle"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
@ -430,6 +445,20 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
|
||||||
|
dependencies = [
|
||||||
|
"android-tzdata",
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clang-sys"
|
name = "clang-sys"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
|
@ -495,6 +524,7 @@ dependencies = [
|
||||||
"axum-server",
|
"axum-server",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"directories",
|
"directories",
|
||||||
"figment",
|
"figment",
|
||||||
|
@ -1296,6 +1326,30 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.62"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
@ -3558,6 +3612,21 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.48.0"
|
version = "0.48.0"
|
||||||
|
|
|
@ -123,6 +123,7 @@ thread_local = "1.1.7"
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
sha-1 = "0.10.1"
|
sha-1 = "0.10.1"
|
||||||
# used for conduit's CLI and admin room command parsing
|
# used for conduit's CLI and admin room command parsing
|
||||||
|
chrono = "0.4"
|
||||||
clap = { version = "4.3.0", default-features = false, features = [
|
clap = { version = "4.3.0", default-features = false, features = [
|
||||||
"derive",
|
"derive",
|
||||||
"error-context",
|
"error-context",
|
||||||
|
|
|
@ -199,6 +199,8 @@ async fn get_content(
|
||||||
allow_remote: bool,
|
allow_remote: bool,
|
||||||
authenticated: bool,
|
authenticated: bool,
|
||||||
) -> Result<get_content::v1::Response, Error> {
|
) -> Result<get_content::v1::Response, Error> {
|
||||||
|
services().media.check_blocked(server_name, &media_id)?;
|
||||||
|
|
||||||
if let Ok(Some(FileMeta {
|
if let Ok(Some(FileMeta {
|
||||||
content_disposition,
|
content_disposition,
|
||||||
content_type,
|
content_type,
|
||||||
|
@ -278,6 +280,8 @@ async fn get_content_as_filename(
|
||||||
allow_remote: bool,
|
allow_remote: bool,
|
||||||
authenticated: bool,
|
authenticated: bool,
|
||||||
) -> Result<get_content_as_filename::v1::Response, Error> {
|
) -> Result<get_content_as_filename::v1::Response, Error> {
|
||||||
|
services().media.check_blocked(server_name, &media_id)?;
|
||||||
|
|
||||||
if let Ok(Some(FileMeta {
|
if let Ok(Some(FileMeta {
|
||||||
file, content_type, ..
|
file, content_type, ..
|
||||||
})) = services()
|
})) = services()
|
||||||
|
@ -371,6 +375,8 @@ async fn get_content_thumbnail(
|
||||||
allow_remote: bool,
|
allow_remote: bool,
|
||||||
authenticated: bool,
|
authenticated: bool,
|
||||||
) -> Result<get_content_thumbnail::v1::Response, Error> {
|
) -> Result<get_content_thumbnail::v1::Response, Error> {
|
||||||
|
services().media.check_blocked(server_name, &media_id)?;
|
||||||
|
|
||||||
if let Some(FileMeta {
|
if let Some(FileMeta {
|
||||||
file,
|
file,
|
||||||
content_type,
|
content_type,
|
||||||
|
|
|
@ -2221,6 +2221,10 @@ pub async fn create_invite_route(
|
||||||
pub async fn get_content_route(
|
pub async fn get_content_route(
|
||||||
body: Ruma<get_content::v1::Request>,
|
body: Ruma<get_content::v1::Request>,
|
||||||
) -> Result<get_content::v1::Response> {
|
) -> Result<get_content::v1::Response> {
|
||||||
|
services()
|
||||||
|
.media
|
||||||
|
.check_blocked(services().globals.server_name(), &body.media_id)?;
|
||||||
|
|
||||||
if let Some(FileMeta {
|
if let Some(FileMeta {
|
||||||
content_disposition,
|
content_disposition,
|
||||||
content_type,
|
content_type,
|
||||||
|
@ -2249,6 +2253,10 @@ pub async fn get_content_route(
|
||||||
pub async fn get_content_thumbnail_route(
|
pub async fn get_content_thumbnail_route(
|
||||||
body: Ruma<get_content_thumbnail::v1::Request>,
|
body: Ruma<get_content_thumbnail::v1::Request>,
|
||||||
) -> Result<get_content_thumbnail::v1::Response> {
|
) -> Result<get_content_thumbnail::v1::Response> {
|
||||||
|
services()
|
||||||
|
.media
|
||||||
|
.check_blocked(services().globals.server_name(), &body.media_id)?;
|
||||||
|
|
||||||
let Some(FileMeta {
|
let Some(FileMeta {
|
||||||
file,
|
file,
|
||||||
content_type,
|
content_type,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{collections::BTreeMap, ops::Range};
|
use std::{collections::BTreeMap, ops::Range, slice::Split};
|
||||||
|
|
||||||
use ruma::{api::client::error::ErrorKind, OwnedServerName, ServerName, UserId};
|
use ruma::{api::client::error::ErrorKind, OwnedServerName, ServerName, UserId};
|
||||||
use sha2::{digest::Output, Sha256};
|
use sha2::{digest::Output, Sha256};
|
||||||
|
@ -6,7 +6,10 @@ use tracing::error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::KeyValueDatabase,
|
database::KeyValueDatabase,
|
||||||
service::{self, media::DbFileMeta},
|
service::{
|
||||||
|
self,
|
||||||
|
media::{BlockedMediaInfo, DbFileMeta},
|
||||||
|
},
|
||||||
utils, Error, Result,
|
utils, Error, Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -20,11 +23,14 @@ impl service::media::Data for KeyValueDatabase {
|
||||||
filename: Option<&str>,
|
filename: Option<&str>,
|
||||||
content_type: Option<&str>,
|
content_type: Option<&str>,
|
||||||
user_id: Option<&UserId>,
|
user_id: Option<&UserId>,
|
||||||
|
is_blocked_filehash: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let metadata = FilehashMetadata::new(file_size);
|
if !is_blocked_filehash {
|
||||||
|
let metadata = FilehashMetadata::new(file_size);
|
||||||
|
|
||||||
self.filehash_metadata
|
self.filehash_metadata
|
||||||
.insert(&sha256_digest, metadata.value())?;
|
.insert(&sha256_digest, metadata.value())?;
|
||||||
|
};
|
||||||
|
|
||||||
let mut key = sha256_digest.to_vec();
|
let mut key = sha256_digest.to_vec();
|
||||||
key.extend_from_slice(servername.as_bytes());
|
key.extend_from_slice(servername.as_bytes());
|
||||||
|
@ -167,9 +173,12 @@ impl service::media::Data for KeyValueDatabase {
|
||||||
value.truncate(32);
|
value.truncate(32);
|
||||||
let sha256_digest = value;
|
let sha256_digest = value;
|
||||||
|
|
||||||
|
let is_blocked = self.is_blocked_filehash(&sha256_digest)?;
|
||||||
let sha256_hex = hex::encode(&sha256_digest);
|
let sha256_hex = hex::encode(&sha256_digest);
|
||||||
|
|
||||||
self.purge_filehash(sha256_digest, false)?;
|
// If the file is blocked, we want to keep the metadata about it so it can be viewed,
|
||||||
|
// as well as filehashes blocked
|
||||||
|
self.purge_filehash(sha256_digest, is_blocked)?;
|
||||||
|
|
||||||
Ok(sha256_hex)
|
Ok(sha256_hex)
|
||||||
};
|
};
|
||||||
|
@ -195,11 +204,14 @@ impl service::media::Data for KeyValueDatabase {
|
||||||
files.push(purge(value));
|
files.push(purge(value));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match self.purge_mediaid(server_name, media_id, false) {
|
match self
|
||||||
Ok(f) => {
|
.is_blocked(server_name, media_id)
|
||||||
|
.map(|is_blocked| self.purge_mediaid(server_name, media_id, is_blocked))
|
||||||
|
{
|
||||||
|
Ok(Ok(f)) => {
|
||||||
files.append(&mut f.into_iter().map(Ok).collect());
|
files.append(&mut f.into_iter().map(Ok).collect());
|
||||||
}
|
}
|
||||||
Err(e) => files.push(Err(e)),
|
Ok(Err(e)) | Err(e) => files.push(Err(e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -221,8 +233,11 @@ impl service::media::Data for KeyValueDatabase {
|
||||||
|
|
||||||
let purge_filehash = |sha256_digest: Vec<u8>| {
|
let purge_filehash = |sha256_digest: Vec<u8>| {
|
||||||
let sha256_hex = hex::encode(&sha256_digest);
|
let sha256_hex = hex::encode(&sha256_digest);
|
||||||
|
let is_blocked = self.is_blocked_filehash(&sha256_digest)?;
|
||||||
|
|
||||||
self.purge_filehash(sha256_digest, false)?;
|
// If the file is blocked, we want to keep the metadata about it so it can be viewed,
|
||||||
|
// as well as filehashes blocked
|
||||||
|
self.purge_filehash(sha256_digest, is_blocked)?;
|
||||||
|
|
||||||
Ok(sha256_hex)
|
Ok(sha256_hex)
|
||||||
};
|
};
|
||||||
|
@ -310,11 +325,15 @@ impl service::media::Data for KeyValueDatabase {
|
||||||
files.push(purge_filehash(sha256_digest));
|
files.push(purge_filehash(sha256_digest));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match self.purge_mediaid(user_id.server_name(), &media_id, false) {
|
match self
|
||||||
Ok(f) => {
|
.is_blocked(user_id.server_name(), &media_id)
|
||||||
|
.map(|is_blocked| {
|
||||||
|
self.purge_mediaid(user_id.server_name(), &media_id, is_blocked)
|
||||||
|
}) {
|
||||||
|
Ok(Ok(f)) => {
|
||||||
files.append(&mut f.into_iter().map(Ok).collect());
|
files.append(&mut f.into_iter().map(Ok).collect());
|
||||||
}
|
}
|
||||||
Err(e) => files.push(Err(e)),
|
Ok(Err(e)) | Err(e) => files.push(Err(e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -355,8 +374,11 @@ impl service::media::Data for KeyValueDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
let sha256_hex = hex::encode(&sha256_digest);
|
let sha256_hex = hex::encode(&sha256_digest);
|
||||||
|
let is_blocked = self.is_blocked_filehash(&sha256_digest)?;
|
||||||
|
|
||||||
self.purge_filehash(sha256_digest, false)?;
|
// If the file is blocked, we want to keep the metadata about it so it can be viewed,
|
||||||
|
// as well as filehashes blocked
|
||||||
|
self.purge_filehash(sha256_digest, is_blocked)?;
|
||||||
|
|
||||||
files.push(Ok(sha256_hex));
|
files.push(Ok(sha256_hex));
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -379,9 +401,11 @@ impl service::media::Data for KeyValueDatabase {
|
||||||
.map(utils::string_from_bytes)?
|
.map(utils::string_from_bytes)?
|
||||||
.map_err(|_| Error::bad_database("Invalid Media ID String in metadata key"))?;
|
.map_err(|_| Error::bad_database("Invalid Media ID String in metadata key"))?;
|
||||||
|
|
||||||
|
let is_blocked = self.is_blocked(&server_name, &media_id)?;
|
||||||
|
|
||||||
files.append(
|
files.append(
|
||||||
&mut self
|
&mut self
|
||||||
.purge_mediaid(&server_name, &media_id, false)?
|
.purge_mediaid(&server_name, &media_id, is_blocked)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(Ok)
|
.map(Ok)
|
||||||
.collect(),
|
.collect(),
|
||||||
|
@ -406,9 +430,363 @@ impl service::media::Data for KeyValueDatabase {
|
||||||
|
|
||||||
files
|
files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_blocked(&self, server_name: &ServerName, media_id: &str) -> Result<bool> {
|
||||||
|
let blocked_via_hash = || {
|
||||||
|
let mut key = server_name.as_bytes().to_vec();
|
||||||
|
key.push(0xff);
|
||||||
|
key.extend_from_slice(media_id.as_bytes());
|
||||||
|
|
||||||
|
let Some(metadata) = self.servernamemediaid_metadata.get(&key)? else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
let sha256_digest = parse_metadata(&metadata).inspect_err(|e| {
|
||||||
|
error!("Error parsing metadata for \"mxc://{server_name}/{media_id}\" from servernamemediaid_metadata: {e}");
|
||||||
|
})?.sha256_digest;
|
||||||
|
|
||||||
|
self.is_blocked_filehash(&sha256_digest)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(self.is_directly_blocked(server_name, media_id)? || blocked_via_hash()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block(
|
||||||
|
&self,
|
||||||
|
media: &[(OwnedServerName, String)],
|
||||||
|
unix_secs: u64,
|
||||||
|
reason: Option<String>,
|
||||||
|
) -> Vec<Error> {
|
||||||
|
let reason = reason.unwrap_or_default();
|
||||||
|
let unix_secs = unix_secs.to_be_bytes();
|
||||||
|
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
for (server_name, media_id) in media {
|
||||||
|
let mut key = server_name.as_bytes().to_vec();
|
||||||
|
key.push(0xff);
|
||||||
|
key.extend_from_slice(media_id.as_bytes());
|
||||||
|
|
||||||
|
let mut value = unix_secs.to_vec();
|
||||||
|
value.extend_from_slice(reason.as_bytes());
|
||||||
|
|
||||||
|
if let Err(e) = self.blocked_servername_mediaid.insert(&key, &value) {
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errors
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_from_user(
|
||||||
|
&self,
|
||||||
|
user_id: &UserId,
|
||||||
|
now: u64,
|
||||||
|
reason: &str,
|
||||||
|
after: Option<u64>,
|
||||||
|
) -> Vec<Error> {
|
||||||
|
let mut prefix = user_id.server_name().as_bytes().to_vec();
|
||||||
|
prefix.push(0xff);
|
||||||
|
prefix.extend_from_slice(user_id.localpart().as_bytes());
|
||||||
|
prefix.push(0xff);
|
||||||
|
|
||||||
|
let mut value = now.to_be_bytes().to_vec();
|
||||||
|
value.extend_from_slice(reason.as_bytes());
|
||||||
|
|
||||||
|
self.servername_userlocalpart_mediaid
|
||||||
|
.scan_prefix(prefix)
|
||||||
|
.map(|(k, _)| {
|
||||||
|
let parts = k.split(|&b| b == 0xff);
|
||||||
|
|
||||||
|
let media_id = parts.last().ok_or_else(|| {
|
||||||
|
Error::bad_database("Invalid format of key in blocked_servername_mediaid")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut key = user_id.server_name().as_bytes().to_vec();
|
||||||
|
key.push(0xff);
|
||||||
|
key.extend_from_slice(media_id);
|
||||||
|
|
||||||
|
let Some(mut meta) = self.servernamemediaid_metadata.get(&key)? else {
|
||||||
|
return Err(Error::bad_database(
|
||||||
|
"Invalid format of metadata in servernamemediaid_metadata",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
meta.truncate(32);
|
||||||
|
let sha256_digest = meta;
|
||||||
|
|
||||||
|
let Some(metadata) = self
|
||||||
|
.filehash_metadata
|
||||||
|
.get(&sha256_digest)?
|
||||||
|
.map(FilehashMetadata::from_vec)
|
||||||
|
else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if after
|
||||||
|
.map(|after| Ok::<bool, Error>(metadata.creation(&sha256_digest)? > after))
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
self.blocked_servername_mediaid.insert(&key, &value)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter_map(Result::err)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unblock(&self, media: &[(OwnedServerName, String)]) -> Vec<Error> {
|
||||||
|
let maybe_remove_remaining_metadata = |metadata: &DbFileMeta, errors: &mut Vec<Error>| {
|
||||||
|
for (k, _) in self
|
||||||
|
.filehash_servername_mediaid
|
||||||
|
.scan_prefix(metadata.sha256_digest.clone())
|
||||||
|
{
|
||||||
|
if let Some(servername_mediaid) = k.get(32..) {
|
||||||
|
if let Err(e) = self.blocked_servername_mediaid.remove(servername_mediaid) {
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!(
|
||||||
|
"Invalid format of key in filehash_servername_mediaid for media with sha256 content hash of {}",
|
||||||
|
hex::encode(&metadata.sha256_digest)
|
||||||
|
);
|
||||||
|
errors.push(Error::BadDatabase(
|
||||||
|
"Invalid format of key in filehash_servername_mediaid",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let thumbnail_id_error = || {
|
||||||
|
error!(
|
||||||
|
"Invalid format of key in filehash_thumbnail_id for media with sha256 content hash of {}",
|
||||||
|
hex::encode(&metadata.sha256_digest)
|
||||||
|
);
|
||||||
|
Error::BadDatabase("Invalid format of value in filehash_thumbnailid")
|
||||||
|
};
|
||||||
|
|
||||||
|
for (k, _) in self
|
||||||
|
.filehash_thumbnailid
|
||||||
|
.scan_prefix(metadata.sha256_digest.clone())
|
||||||
|
{
|
||||||
|
if let Some(end) = k.len().checked_sub(9) {
|
||||||
|
if let Some(servername_mediaid) = k.get(32..end) {
|
||||||
|
if let Err(e) = self.blocked_servername_mediaid.remove(servername_mediaid) {
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errors.push(thumbnail_id_error());
|
||||||
|
}
|
||||||
|
errors.push(thumbnail_id_error());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have the actual file downloaded anymore, remove the remaining
|
||||||
|
// metadata of the file
|
||||||
|
match self
|
||||||
|
.filehash_metadata
|
||||||
|
.get(&metadata.sha256_digest)
|
||||||
|
.map(|opt| opt.is_none())
|
||||||
|
{
|
||||||
|
Err(e) => errors.push(e),
|
||||||
|
Ok(true) => {
|
||||||
|
if let Err(e) = self.purge_filehash(metadata.sha256_digest.clone(), false) {
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false) => (),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
for (server_name, media_id) in media {
|
||||||
|
let mut key = server_name.as_bytes().to_vec();
|
||||||
|
key.push(0xff);
|
||||||
|
key.extend_from_slice(media_id.as_bytes());
|
||||||
|
|
||||||
|
match self
|
||||||
|
.servernamemediaid_metadata
|
||||||
|
.get(&key)
|
||||||
|
.map(|opt| opt.as_deref().map(parse_metadata))
|
||||||
|
{
|
||||||
|
Err(e) => {
|
||||||
|
errors.push(e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Ok(None) => (),
|
||||||
|
Ok(Some(Err(e))) => {
|
||||||
|
error!("Error parsing metadata for \"mxc://{server_name}/{media_id}\" from servernamemediaid_metadata: {e}");
|
||||||
|
errors.push(e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Ok(Some(Ok(metadata))) => {
|
||||||
|
maybe_remove_remaining_metadata(&metadata, &mut errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key.push(0xff);
|
||||||
|
for (_, v) in self.thumbnailid_metadata.scan_prefix(key) {
|
||||||
|
match parse_metadata(&v) {
|
||||||
|
Ok(metadata) => {
|
||||||
|
maybe_remove_remaining_metadata(&metadata, &mut errors);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error parsing metadata for thumbnail of \"mxc://{server_name}/{media_id}\" from thumbnailid_metadata: {e}");
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errors
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_blocked(&self) -> Vec<Result<BlockedMediaInfo>> {
|
||||||
|
let parse_servername = |parts: &mut Split<_, _>| {
|
||||||
|
OwnedServerName::try_from(
|
||||||
|
utils::string_from_bytes(parts.next().ok_or_else(|| {
|
||||||
|
Error::BadDatabase("Invalid format of metadata of blocked media")
|
||||||
|
})?)
|
||||||
|
.map_err(|_| Error::BadDatabase("Invalid server_name String of blocked data"))?,
|
||||||
|
)
|
||||||
|
.map_err(|_| Error::BadDatabase("Invalid ServerName in blocked_servername_mediaid"))
|
||||||
|
};
|
||||||
|
|
||||||
|
let parse_string =
|
||||||
|
|parts: &mut Split<_, _>| {
|
||||||
|
utils::string_from_bytes(parts.next().ok_or_else(|| {
|
||||||
|
Error::BadDatabase("Invalid format of metadata of blocked media")
|
||||||
|
})?)
|
||||||
|
.map_err(|_| Error::BadDatabase("Invalid string in blocked media metadata"))
|
||||||
|
};
|
||||||
|
|
||||||
|
let splitter = |b: &u8| *b == 0xff;
|
||||||
|
|
||||||
|
self.blocked_servername_mediaid
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| {
|
||||||
|
let mut parts = k.split(splitter);
|
||||||
|
|
||||||
|
// Using map_err, as inspect_err causes lifetime issues
|
||||||
|
// "implementation of `FnOnce` is not general enough"
|
||||||
|
let log_error = |e| {
|
||||||
|
error!("Error parsing key of blocked media: {e}");
|
||||||
|
e
|
||||||
|
};
|
||||||
|
|
||||||
|
let server_name = parse_servername(&mut parts).map_err(log_error)?;
|
||||||
|
|
||||||
|
let media_id = parse_string(&mut parts).map_err(log_error)?;
|
||||||
|
|
||||||
|
let (unix_secs, reason) = v
|
||||||
|
.split_at_checked(8)
|
||||||
|
.map(|(secs, reason)| -> Result<(u64, Option<String>)> {
|
||||||
|
Ok((
|
||||||
|
secs.try_into()
|
||||||
|
.map_err(|_| {
|
||||||
|
Error::bad_database(
|
||||||
|
"Invalid block time in blocked_servername_mediaid ",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(u64::from_be_bytes)?,
|
||||||
|
if reason.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(utils::string_from_bytes(reason).map_err(|_| {
|
||||||
|
Error::bad_database("Invalid string in blocked media metadata")
|
||||||
|
})?)
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::bad_database("Invalid format of value in blocked_servername_mediaid")
|
||||||
|
})??;
|
||||||
|
|
||||||
|
let sha256_hex = self.servernamemediaid_metadata.get(&k)?.map(|mut meta| {
|
||||||
|
meta.truncate(32);
|
||||||
|
hex::encode(meta)
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(BlockedMediaInfo {
|
||||||
|
server_name,
|
||||||
|
media_id,
|
||||||
|
unix_secs,
|
||||||
|
reason,
|
||||||
|
sha256_hex,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_blocked_filehash(&self, sha256_digest: &[u8]) -> Result<bool> {
|
||||||
|
for (filehash_servername_mediaid, _) in self
|
||||||
|
.filehash_servername_mediaid
|
||||||
|
.scan_prefix(sha256_digest.to_owned())
|
||||||
|
{
|
||||||
|
let servername_mediaid = filehash_servername_mediaid.get(32..).ok_or_else(|| {
|
||||||
|
error!(
|
||||||
|
"Invalid format of key in filehash_servername_mediaid for media with sha256 content hash of {}",
|
||||||
|
hex::encode(sha256_digest)
|
||||||
|
);
|
||||||
|
Error::BadDatabase("Invalid format of key in filehash_servername_mediaid")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if self
|
||||||
|
.blocked_servername_mediaid
|
||||||
|
.get(servername_mediaid)?
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let thumbnail_id_error = || {
|
||||||
|
error!(
|
||||||
|
"Invalid format of key in filehash_thumbnail_id for media with sha256 content hash of {}",
|
||||||
|
hex::encode(sha256_digest)
|
||||||
|
);
|
||||||
|
Error::BadDatabase("Invalid format of value in filehash_thumbnailid")
|
||||||
|
};
|
||||||
|
|
||||||
|
for (thumbnail_id, _) in self
|
||||||
|
.filehash_thumbnailid
|
||||||
|
.scan_prefix(sha256_digest.to_owned())
|
||||||
|
{
|
||||||
|
let servername_mediaid = thumbnail_id
|
||||||
|
.get(
|
||||||
|
32..thumbnail_id
|
||||||
|
.len()
|
||||||
|
.checked_sub(9)
|
||||||
|
.ok_or_else(thumbnail_id_error)?,
|
||||||
|
)
|
||||||
|
.ok_or_else(thumbnail_id_error)?;
|
||||||
|
|
||||||
|
if self
|
||||||
|
.blocked_servername_mediaid
|
||||||
|
.get(servername_mediaid)?
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeyValueDatabase {
|
impl KeyValueDatabase {
|
||||||
|
/// Only checks whether the media id itself is blocked, and not associated filehashes
|
||||||
|
fn is_directly_blocked(&self, server_name: &ServerName, media_id: &str) -> Result<bool> {
|
||||||
|
let mut key = server_name.as_bytes().to_vec();
|
||||||
|
key.push(0xff);
|
||||||
|
key.extend_from_slice(media_id.as_bytes());
|
||||||
|
|
||||||
|
self.blocked_servername_mediaid
|
||||||
|
.get(&key)
|
||||||
|
.map(|x| x.is_some())
|
||||||
|
}
|
||||||
|
|
||||||
fn purge_mediaid(
|
fn purge_mediaid(
|
||||||
&self,
|
&self,
|
||||||
server_name: &ServerName,
|
server_name: &ServerName,
|
||||||
|
|
|
@ -179,6 +179,7 @@ pub struct KeyValueDatabase {
|
||||||
pub(super) servernamemediaid_metadata: Arc<dyn KvTree>, // Servername + MediaID -> content sha256 + Filename + ContentType + extra 0xff byte if media is allowed on unauthenticated endpoints
|
pub(super) servernamemediaid_metadata: Arc<dyn KvTree>, // Servername + MediaID -> content sha256 + Filename + ContentType + extra 0xff byte if media is allowed on unauthenticated endpoints
|
||||||
pub(super) filehash_servername_mediaid: Arc<dyn KvTree>, // sha256 of content + Servername + MediaID, used to delete dangling references to filehashes from servernamemediaid
|
pub(super) filehash_servername_mediaid: Arc<dyn KvTree>, // sha256 of content + Servername + MediaID, used to delete dangling references to filehashes from servernamemediaid
|
||||||
pub(super) filehash_metadata: Arc<dyn KvTree>, // sha256 of content -> file size + creation time + last access time
|
pub(super) filehash_metadata: Arc<dyn KvTree>, // sha256 of content -> file size + creation time + last access time
|
||||||
|
pub(super) blocked_servername_mediaid: Arc<dyn KvTree>, // Servername + MediaID of blocked media -> time of block + reason
|
||||||
pub(super) servername_userlocalpart_mediaid: Arc<dyn KvTree>, // Servername + User Localpart + MediaID
|
pub(super) servername_userlocalpart_mediaid: Arc<dyn KvTree>, // Servername + User Localpart + MediaID
|
||||||
pub(super) servernamemediaid_userlocalpart: Arc<dyn KvTree>, // Servername + MediaID -> User Localpart, used to remove keys from above when files are deleted by unrelated means
|
pub(super) servernamemediaid_userlocalpart: Arc<dyn KvTree>, // Servername + MediaID -> User Localpart, used to remove keys from above when files are deleted by unrelated means
|
||||||
pub(super) thumbnailid_metadata: Arc<dyn KvTree>, // ThumbnailId = Servername + MediaID + width + height -> Filename + ContentType + extra 0xff byte if media is allowed on unauthenticated endpoints
|
pub(super) thumbnailid_metadata: Arc<dyn KvTree>, // ThumbnailId = Servername + MediaID + width + height -> Filename + ContentType + extra 0xff byte if media is allowed on unauthenticated endpoints
|
||||||
|
@ -389,6 +390,7 @@ impl KeyValueDatabase {
|
||||||
servernamemediaid_metadata: builder.open_tree("servernamemediaid_metadata")?,
|
servernamemediaid_metadata: builder.open_tree("servernamemediaid_metadata")?,
|
||||||
filehash_servername_mediaid: builder.open_tree("filehash_servername_mediaid")?,
|
filehash_servername_mediaid: builder.open_tree("filehash_servername_mediaid")?,
|
||||||
filehash_metadata: builder.open_tree("filehash_metadata")?,
|
filehash_metadata: builder.open_tree("filehash_metadata")?,
|
||||||
|
blocked_servername_mediaid: builder.open_tree("blocked_servername_mediaid")?,
|
||||||
servername_userlocalpart_mediaid: builder
|
servername_userlocalpart_mediaid: builder
|
||||||
.open_tree("servername_userlocalpart_mediaid")?,
|
.open_tree("servername_userlocalpart_mediaid")?,
|
||||||
servernamemediaid_userlocalpart: builder
|
servernamemediaid_userlocalpart: builder
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
convert::TryFrom,
|
convert::TryFrom,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use chrono::DateTime;
|
||||||
use clap::{Args, Parser};
|
use clap::{Args, Parser};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
|
@ -37,7 +39,7 @@ use crate::{
|
||||||
Error, PduEvent, Result,
|
Error, PduEvent, Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::pdu::PduBuilder;
|
use super::{media::BlockedMediaInfo, pdu::PduBuilder};
|
||||||
|
|
||||||
#[cfg_attr(test, derive(Debug))]
|
#[cfg_attr(test, derive(Debug))]
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
@ -180,6 +182,55 @@ enum AdminCommand {
|
||||||
force_filehash: bool,
|
force_filehash: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Prevents the list of media from being accessed, but does not delete the media if it
|
||||||
|
/// is already downloaded. If the media has already been downloaded, the sha256 hash
|
||||||
|
/// is blocked, meaning that any other current or future uploads/downloads of the exact same
|
||||||
|
/// content cannot be accessed either.
|
||||||
|
///
|
||||||
|
/// There should be one MXC URI per line, all contained within a code-block
|
||||||
|
BlockMedia {
|
||||||
|
#[arg(long, short)]
|
||||||
|
/// Prevents the specified media from being downloaded in the future
|
||||||
|
///
|
||||||
|
/// Note: This will also delete identical media uploaded by other users, so
|
||||||
|
/// only use this all the media is known to be undesirable
|
||||||
|
and_purge: bool,
|
||||||
|
#[arg(long, short)]
|
||||||
|
/// Optional reason as to why this media should be blocked
|
||||||
|
reason: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Prevents all media uploaded by the local users, listed in a code-block, from being accessed
|
||||||
|
///
|
||||||
|
/// Note: This will also block media with the same SHA256 hash, so
|
||||||
|
/// only use this when all media uploaded by the user is undesirable (or if
|
||||||
|
/// you only plan for the bloackage to be temporary)
|
||||||
|
BlockMediaFromUsers {
|
||||||
|
#[arg(
|
||||||
|
long, short,
|
||||||
|
value_parser = humantime::parse_duration
|
||||||
|
)]
|
||||||
|
/// Only block media uploaded in the last {timeframe}
|
||||||
|
///
|
||||||
|
/// Should be in the form specified by humantime::parse_duration
|
||||||
|
/// (e.g. 48h, 60min, 10days etc.)
|
||||||
|
// --help is unformatted
|
||||||
|
#[allow(rustdoc::bare_urls)]
|
||||||
|
/// https://docs.rs/humantime/2.2.0/humantime/fn.parse_duration.html
|
||||||
|
from_last: Option<Duration>,
|
||||||
|
#[arg(long, short)]
|
||||||
|
/// Optional reason as to why this media should be blocked
|
||||||
|
reason: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Lists all media that is currently blocked
|
||||||
|
ListBlockedMedia,
|
||||||
|
|
||||||
|
/// Allows previously blocked media to be accessed again. Will also unblock media with the
|
||||||
|
/// same SHA256 hash
|
||||||
|
/// There should be one MXC URI per line, all contained within a code-block
|
||||||
|
UnblockMedia,
|
||||||
|
|
||||||
/// Get the auth_chain of a PDU
|
/// Get the auth_chain of a PDU
|
||||||
GetAuthChain {
|
GetAuthChain {
|
||||||
/// An event ID (the $ character followed by the base64 reference hash)
|
/// An event ID (the $ character followed by the base64 reference hash)
|
||||||
|
@ -986,6 +1037,121 @@ impl Service {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AdminCommand::BlockMedia { and_purge, reason } => media_from_body(body).map_or_else(
|
||||||
|
|message| message,
|
||||||
|
|media| {
|
||||||
|
let failed_count = services().media.block(&media, reason).len();
|
||||||
|
let failed_purge_count = if and_purge {
|
||||||
|
services().media.purge(&media, true).len()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
match (failed_count == 0, failed_purge_count == 0) {
|
||||||
|
(true, true) => RoomMessageEventContent::text_plain("Successfully blocked media"),
|
||||||
|
(false, true) => RoomMessageEventContent::text_plain(format!(
|
||||||
|
"Failed to block {failed_count} media, check logs for more details"
|
||||||
|
)),
|
||||||
|
(true, false ) => RoomMessageEventContent::text_plain(format!(
|
||||||
|
"Failed to purge {failed_purge_count} media, check logs for more details"
|
||||||
|
)),
|
||||||
|
(false, false) => RoomMessageEventContent::text_plain(format!(
|
||||||
|
"Failed to block {failed_count}, and purge {failed_purge_count} media, check logs for more details"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
AdminCommand::BlockMediaFromUsers { from_last, reason } => {
|
||||||
|
let after = from_last.map(unix_secs_from_duration).transpose()?;
|
||||||
|
|
||||||
|
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```"
|
||||||
|
{
|
||||||
|
let user_ids = match userids_from_body(&body)? {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(message) => return Ok(message),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut failed_count = 0;
|
||||||
|
|
||||||
|
for user_id in user_ids {
|
||||||
|
let reason = reason.as_ref().map_or_else(
|
||||||
|
|| Cow::Owned(format!("uploaded by {user_id}")),
|
||||||
|
Cow::Borrowed,
|
||||||
|
);
|
||||||
|
|
||||||
|
failed_count += services()
|
||||||
|
.media
|
||||||
|
.block_from_user(user_id, &reason, after)
|
||||||
|
.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
if failed_count == 0 {
|
||||||
|
RoomMessageEventContent::text_plain("Successfully blocked media")
|
||||||
|
} else {
|
||||||
|
RoomMessageEventContent::text_plain(format!(
|
||||||
|
"Failed to block {failed_count} media, check logs for more details"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RoomMessageEventContent::text_plain(
|
||||||
|
"Expected code block in command body. Add --help for details.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AdminCommand::ListBlockedMedia => {
|
||||||
|
let mut markdown_message = String::from(
|
||||||
|
"| SHA256 hash | MXC URI | Time Blocked | Reason |\n| --- | --- | --- | --- |",
|
||||||
|
);
|
||||||
|
let mut html_message = String::from(
|
||||||
|
r#"<table><thead><tr><th scope="col">SHA256 hash</th><th scope="col">MXC URI</th><th scope="col">Time Blocked</th><th scope="col">Reason</th></tr></thead><tbody>"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
for media in services().media.list_blocked() {
|
||||||
|
let Ok(BlockedMediaInfo {
|
||||||
|
server_name,
|
||||||
|
media_id,
|
||||||
|
unix_secs,
|
||||||
|
reason,
|
||||||
|
sha256_hex,
|
||||||
|
}) = media else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let sha256_hex = sha256_hex.unwrap_or_default();
|
||||||
|
let reason = reason.unwrap_or_default();
|
||||||
|
|
||||||
|
let time = i64::try_from(unix_secs)
|
||||||
|
.map(|unix_secs| DateTime::from_timestamp(unix_secs, 0))
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.expect("Time is valid");
|
||||||
|
|
||||||
|
markdown_message
|
||||||
|
.push_str(&format!("\n| {sha256_hex} | mxc://{server_name}/{media_id} | {time} | {reason} |"));
|
||||||
|
|
||||||
|
html_message.push_str(&format!(
|
||||||
|
"<tr><td>{sha256_hex}</td><td>mxc://{server_name}/{media_id}</td><td>{time}</td><td>{reason}</td></tr>",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
html_message.push_str("</tbody></table>");
|
||||||
|
|
||||||
|
RoomMessageEventContent::text_html(markdown_message, html_message)
|
||||||
|
}
|
||||||
|
AdminCommand::UnblockMedia => media_from_body(body).map_or_else(
|
||||||
|
|message| message,
|
||||||
|
|media| {
|
||||||
|
let failed_count = services().media.unblock(&media).len();
|
||||||
|
|
||||||
|
if failed_count == 0 {
|
||||||
|
RoomMessageEventContent::text_plain("Successfully unblocked media")
|
||||||
|
} else {
|
||||||
|
RoomMessageEventContent::text_plain(format!(
|
||||||
|
"Failed to unblock {failed_count} media, check logs for more details"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
AdminCommand::SignJson => {
|
AdminCommand::SignJson => {
|
||||||
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```"
|
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```"
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
use ruma::{OwnedServerName, ServerName, UserId};
|
use ruma::{OwnedServerName, ServerName, UserId};
|
||||||
use sha2::{digest::Output, Sha256};
|
use sha2::{digest::Output, Sha256};
|
||||||
|
|
||||||
use crate::Result;
|
use crate::{Error, Result};
|
||||||
|
|
||||||
|
use super::BlockedMediaInfo;
|
||||||
|
|
||||||
use super::DbFileMeta;
|
use super::DbFileMeta;
|
||||||
|
|
||||||
|
@ -16,6 +18,7 @@ pub trait Data: Send + Sync {
|
||||||
filename: Option<&str>,
|
filename: Option<&str>,
|
||||||
content_type: Option<&str>,
|
content_type: Option<&str>,
|
||||||
user_id: Option<&UserId>,
|
user_id: Option<&UserId>,
|
||||||
|
is_blocked_filehash: bool,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
fn search_file_metadata(&self, servername: &ServerName, media_id: &str) -> Result<DbFileMeta>;
|
fn search_file_metadata(&self, servername: &ServerName, media_id: &str) -> Result<DbFileMeta>;
|
||||||
|
@ -62,4 +65,32 @@ pub trait Data: Send + Sync {
|
||||||
force_filehash: bool,
|
force_filehash: bool,
|
||||||
after: Option<u64>,
|
after: Option<u64>,
|
||||||
) -> Vec<Result<String>>;
|
) -> Vec<Result<String>>;
|
||||||
|
|
||||||
|
fn is_blocked(&self, server_name: &ServerName, media_id: &str) -> Result<bool>;
|
||||||
|
|
||||||
|
fn block(
|
||||||
|
&self,
|
||||||
|
media: &[(OwnedServerName, String)],
|
||||||
|
unix_secs: u64,
|
||||||
|
reason: Option<String>,
|
||||||
|
) -> Vec<Error>;
|
||||||
|
|
||||||
|
fn block_from_user(
|
||||||
|
&self,
|
||||||
|
user_id: &UserId,
|
||||||
|
now: u64,
|
||||||
|
reason: &str,
|
||||||
|
after: Option<u64>,
|
||||||
|
) -> Vec<Error>;
|
||||||
|
|
||||||
|
fn unblock(&self, media: &[(OwnedServerName, String)]) -> Vec<Error>;
|
||||||
|
|
||||||
|
/// Returns a Vec of:
|
||||||
|
/// - The server the media is from
|
||||||
|
/// - The media id
|
||||||
|
/// - The time it was blocked, in unix seconds
|
||||||
|
/// - The optional reason why it was blocked
|
||||||
|
fn list_blocked(&self) -> Vec<Result<BlockedMediaInfo>>;
|
||||||
|
|
||||||
|
fn is_blocked_filehash(&self, sha256_digest: &[u8]) -> Result<bool>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ use tracing::error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{DirectoryStructure, MediaConfig},
|
config::{DirectoryStructure, MediaConfig},
|
||||||
services, Error, Result,
|
services, utils, Error, Result,
|
||||||
};
|
};
|
||||||
use image::imageops::FilterType;
|
use image::imageops::FilterType;
|
||||||
|
|
||||||
|
@ -38,6 +38,14 @@ pub struct Service {
|
||||||
pub db: &'static dyn Data,
|
pub db: &'static dyn Data,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct BlockedMediaInfo {
|
||||||
|
pub server_name: OwnedServerName,
|
||||||
|
pub media_id: String,
|
||||||
|
pub unix_secs: u64,
|
||||||
|
pub reason: Option<String>,
|
||||||
|
pub sha256_hex: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Service {
|
impl Service {
|
||||||
/// Uploads a file.
|
/// Uploads a file.
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
|
@ -59,9 +67,16 @@ impl Service {
|
||||||
filename,
|
filename,
|
||||||
content_type,
|
content_type,
|
||||||
user_id,
|
user_id,
|
||||||
|
self.db.is_blocked_filehash(&sha256_digest)?,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
create_file(&sha256_hex, file).await
|
if !self.db.is_blocked_filehash(&sha256_digest)? {
|
||||||
|
create_file(&sha256_hex, file).await
|
||||||
|
} else if user_id.is_none() {
|
||||||
|
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Uploads or replaces a file thumbnail.
|
/// Uploads or replaces a file thumbnail.
|
||||||
|
@ -358,6 +373,49 @@ impl Service {
|
||||||
|
|
||||||
purge_files(hashes)
|
purge_files(hashes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks whether the media has been blocked by administrators, returning either
|
||||||
|
/// a database error, or a not found error if it is blocked
|
||||||
|
pub fn check_blocked(&self, server_name: &ServerName, media_id: &str) -> Result<()> {
|
||||||
|
if self.db.is_blocked(server_name, media_id)? {
|
||||||
|
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks the specified media as blocked, preventing them from being accessed
|
||||||
|
pub fn block(&self, media: &[(OwnedServerName, String)], reason: Option<String>) -> Vec<Error> {
|
||||||
|
let now = utils::secs_since_unix_epoch();
|
||||||
|
|
||||||
|
self.db.block(media, now, reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks the media uploaded by a local user as blocked, preventing it from being accessed
|
||||||
|
pub fn block_from_user(
|
||||||
|
&self,
|
||||||
|
user_id: &UserId,
|
||||||
|
reason: &str,
|
||||||
|
after: Option<u64>,
|
||||||
|
) -> Vec<Error> {
|
||||||
|
let now = utils::secs_since_unix_epoch();
|
||||||
|
|
||||||
|
self.db.block_from_user(user_id, now, reason, after)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unblocks the specified media, allowing them from being accessed again
|
||||||
|
pub fn unblock(&self, media: &[(OwnedServerName, String)]) -> Vec<Error> {
|
||||||
|
self.db.unblock(media)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a Vec of:
|
||||||
|
/// - The server the media is from
|
||||||
|
/// - The media id
|
||||||
|
/// - The time it was blocked, in unix seconds
|
||||||
|
/// - The optional reason why it was blocked
|
||||||
|
pub fn list_blocked(&self) -> Vec<Result<BlockedMediaInfo>> {
|
||||||
|
self.db.list_blocked()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates the media file, using the configured media backend
|
/// Creates the media file, using the configured media backend
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue