feat: super basic file uploads for logged in users

this is super crude and unauthorized!!! also vulnerable to AP injections
so much more work has to be done
This commit is contained in:
əlemi 2024-11-09 14:41:19 +01:00
parent 005524201d
commit 6793f0fdc9
Signed by: alemi
GPG key ID: A4895B84D311642C
5 changed files with 78 additions and 4 deletions

View file

@ -15,6 +15,9 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub compat: CompatibilityConfig, pub compat: CompatibilityConfig,
#[serde(default)]
pub files: FileStorageConfig,
// TODO should i move app keys here? // TODO should i move app keys here?
} }
@ -115,6 +118,13 @@ pub struct CompatibilityConfig {
pub skip_single_attachment_if_image_is_set: bool, pub skip_single_attachment_if_image_is_set: bool,
} }
#[serde_inline_default::serde_inline_default]
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, serde_default::DefaultFromSerde)]
pub struct FileStorageConfig {
#[serde_inline_default("files/".to_string())]
pub path: String,
}
impl Config { impl Config {
pub fn load(path: Option<&std::path::PathBuf>) -> Self { pub fn load(path: Option<&std::path::PathBuf>) -> Self {
let Some(cfg_path) = path else { return Config::default() }; let Some(cfg_path) = path else { return Config::default() };

View file

@ -22,7 +22,7 @@ jrd = "0.1"
tracing = "0.1" tracing = "0.1"
tokio = { version = "1.40", features = ["full"] } # TODO slim this down tokio = { version = "1.40", features = ["full"] } # TODO slim this down
reqwest = { version = "0.12", features = ["json"] } reqwest = { version = "0.12", features = ["json"] }
axum = "0.7" axum = { version = "0.7", features = ["multipart"] }
tower-http = { version = "0.5", features = ["cors", "trace"] } tower-http = { version = "0.5", features = ["cors", "trace"] }
httpsign = { path = "../../utils/httpsign/", features = ["axum"] } httpsign = { path = "../../utils/httpsign/", features = ["axum"] }
apb = { path = "../../apb", features = ["unstructured", "orm", "activitypub-fe", "activitypub-counters", "litepub", "ostatus", "toot", "jsonld"] } apb = { path = "../../apb", features = ["unstructured", "orm", "activitypub-fe", "activitypub-counters", "litepub", "ostatus", "toot", "jsonld"] }

View file

@ -0,0 +1,60 @@
use axum::extract::{Multipart, Path, State};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use upub::Context;
use crate::AuthIdentity;
pub async fn upload(
State(ctx): State<Context>,
AuthIdentity(auth): AuthIdentity,
mut multipart: Multipart,
) -> crate::ApiResult<()> {
if !auth.is_local() {
return Err(crate::ApiError::forbidden());
}
let mut uploaded_something = false;
while let Some(field) = multipart.next_field().await.unwrap() {
let _ = if let Some(filename) = field.file_name() {
filename.to_string()
} else {
tracing::warn!("skipping multipart field {field:?}");
continue;
};
let data = match field.bytes().await {
Ok(x) => x,
Err(e) => {
tracing::error!("error reading multipart part: {e:?}");
continue;
},
};
let name = sha256::digest(data.as_ref());
let path = format!("{}{name}", ctx.cfg().files.path);
tokio::fs::File::open(path).await?.write_all(&data).await?;
uploaded_something = true;
}
if uploaded_something {
Ok(())
} else {
Err(crate::ApiError::bad_request())
}
}
pub async fn download(
State(ctx): State<Context>,
AuthIdentity(_auth): AuthIdentity,
Path(id): Path<String>,
) -> crate::ApiResult<Vec<u8>> {
let path = format!("{}{id}", ctx.cfg().files.path);
let mut buffer = Vec::new();
tokio::fs::File::open(path)
.await?
.read_to_end(&mut buffer)
.await?;
Ok(buffer)
}

View file

@ -6,6 +6,7 @@ pub mod activity;
pub mod application; pub mod application;
pub mod auth; pub mod auth;
pub mod tags; pub mod tags;
pub mod file;
pub mod well_known; pub mod well_known;
use axum::{http::StatusCode, response::IntoResponse, routing::{get, patch, post, put}, Router}; use axum::{http::StatusCode, response::IntoResponse, routing::{get, patch, post, put}, Router};
@ -69,13 +70,13 @@ impl ActivityPubRouter for Router<upub::Context> {
.route("/objects/:id/replies/page", get(ap::object::replies::page)) .route("/objects/:id/replies/page", get(ap::object::replies::page))
.route("/objects/:id/context", get(ap::object::context::get)) .route("/objects/:id/context", get(ap::object::context::get))
.route("/objects/:id/context/page", get(ap::object::context::page)) .route("/objects/:id/context/page", get(ap::object::context::page))
// file routes
.route("/file", post(ap::file::upload))
.route("/file/:id", get(ap::file::download))
//.route("/objects/:id/likes", get(ap::object::likes::get)) //.route("/objects/:id/likes", get(ap::object::likes::get))
//.route("/objects/:id/likes/page", get(ap::object::likes::page)) //.route("/objects/:id/likes/page", get(ap::object::likes::page))
//.route("/objects/:id/shares", get(ap::object::announces::get)) //.route("/objects/:id/shares", get(ap::object::announces::get))
//.route("/objects/:id/shares/page", get(ap::object::announces::page)) //.route("/objects/:id/shares/page", get(ap::object::announces::page))
// hashtags routes
//.route("/hashtags/:name", get(ap::hashtags::get))
//.route("/hashtags/:name/page", get(ap::hashtags::page))
} }
} }

View file

@ -19,6 +19,9 @@ pub enum ApiError {
#[error("fetch error: {0:?}")] #[error("fetch error: {0:?}")]
FetchError(#[from] upub::traits::fetch::RequestError), FetchError(#[from] upub::traits::fetch::RequestError),
#[error("error interacting with file system: {0:?}")]
IO(#[from] std::io::Error),
// wrapper error to return arbitraty status codes // wrapper error to return arbitraty status codes
#[error("{0}")] #[error("{0}")]
Status(StatusCode), Status(StatusCode),