Compare commits

...

2 Commits

Author SHA1 Message Date
Savanni D'Gerinel 5b7faf556f Handle file uploads with a validated session 2023-10-03 17:47:54 -04:00
Savanni D'Gerinel f2bbb4e720 Validate the session token with file uploads
File uploads now check the session token before continuing.

Resolves: https://www.pivotaltracker.com/story/show/186174680
2023-10-03 17:30:43 -04:00
3 changed files with 157 additions and 102 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ result
file-service/*.sqlite file-service/*.sqlite
file-service/*.sqlite-shm file-service/*.sqlite-shm
file-service/*.sqlite-wal file-service/*.sqlite-wal
file-service/var

View File

@ -1,7 +1,12 @@
use build_html::Html; use build_html::Html;
use bytes::Buf;
use cookie::time::error::Format;
use file_service::WriteFileError;
use futures_util::StreamExt;
use http::{Error, StatusCode}; use http::{Error, StatusCode};
use std::collections::HashMap; use std::collections::HashMap;
use warp::http::Response; use std::io::Read;
use warp::{filters::multipart::FormData, http::Response, multipart::Part};
use crate::{pages, App, AuthToken, FileId, FileInfo, ReadFileError, SessionToken}; use crate::{pages, App, AuthToken, FileId, FileInfo, ReadFileError, SessionToken};
@ -98,13 +103,30 @@ pub async fn handle_auth(
} }
pub async fn handle_upload( pub async fn handle_upload(
_app: App, app: App,
_token: SessionToken, token: SessionToken,
form: FormData,
) -> Result<http::Response<String>, Error> { ) -> Result<http::Response<String>, Error> {
println!("handle_upload"); match app.validate_session(token).await {
Response::builder() Ok(Some(_)) => match process_file_upload(app, form).await {
.status(StatusCode::NOT_IMPLEMENTED) Ok(_) => Response::builder()
.body("".to_owned()) .header("location", "/")
.status(StatusCode::SEE_OTHER)
.body("".to_owned()),
Err(UploadError::FilenameMissing) => Response::builder()
.status(StatusCode::BAD_REQUEST)
.body("filename is required for all files".to_owned()),
Err(UploadError::WriteFileError(err)) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(format!("could not write to the file system: {:?}", err)),
Err(UploadError::WarpError(err)) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(format!("error with the app framework: {:?}", err)),
},
_ => Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body("".to_owned()),
}
} }
fn serve_file<F>( fn serve_file<F>(
@ -132,3 +154,102 @@ where
}, },
} }
} }
async fn collect_multipart(
mut stream: warp::filters::multipart::FormData,
) -> Result<Vec<(Option<String>, Option<String>, Vec<u8>)>, warp::Error> {
let mut content: Vec<(Option<String>, Option<String>, Vec<u8>)> = Vec::new();
while let Some(part) = stream.next().await {
match part {
Ok(part) => content.push(collect_content(part).await.unwrap()),
Err(err) => return Err(err),
}
}
Ok(content)
}
async fn collect_content(
mut part: Part,
) -> Result<(Option<String>, Option<String>, Vec<u8>), String> {
let mut content: Vec<u8> = Vec::new();
while let Some(Ok(data)) = part.data().await {
let mut reader = data.reader();
reader.read_to_end(&mut content).unwrap();
}
Ok((
part.content_type().map(|s| s.to_owned()),
part.filename().map(|s| s.to_owned()),
content,
))
}
/*
async fn handle_upload(
form: warp::filters::multipart::FormData,
app: App,
) -> warp::http::Result<warp::http::Response<String>> {
let files = collect_multipart(form).await;
match files {
Ok(files) => {
for (_, filename, content) in files {
match filename {
Some(filename) => {
app.add_file(filename, content).unwrap();
}
None => {
return warp::http::Response::builder()
.status(StatusCode::BAD_REQUEST)
.body("".to_owned())
}
}
}
}
Err(_err) => {
return warp::http::Response::builder()
.status(StatusCode::BAD_REQUEST)
.body("".to_owned())
}
}
// println!("file length: {:?}", files.map(|f| f.len()));
warp::http::Response::builder()
.header("location", "/")
.status(StatusCode::SEE_OTHER)
.body("".to_owned())
}
*/
enum UploadError {
FilenameMissing,
WriteFileError(WriteFileError),
WarpError(warp::Error),
}
impl From<WriteFileError> for UploadError {
fn from(err: WriteFileError) -> Self {
Self::WriteFileError(err)
}
}
impl From<warp::Error> for UploadError {
fn from(err: warp::Error) -> Self {
Self::WarpError(err)
}
}
async fn process_file_upload(app: App, form: FormData) -> Result<(), UploadError> {
let files = collect_multipart(form).await?;
for (_, filename, content) in files {
match filename {
Some(filename) => {
app.add_file(filename, content).await?;
}
None => return Err(UploadError::FilenameMissing),
}
}
Ok(())
}

View File

@ -42,75 +42,12 @@ async fn authenticate_user(app: App, auth_token: String) -> Result<Username, war
*/ */
/* /*
async fn collect_content(
mut part: Part,
) -> Result<(Option<String>, Option<String>, Vec<u8>), String> {
let mut content: Vec<u8> = Vec::new();
while let Some(Ok(data)) = part.data().await {
let mut reader = data.reader();
reader.read_to_end(&mut content).unwrap();
}
Ok((
part.content_type().map(|s| s.to_owned()),
part.filename().map(|s| s.to_owned()),
content,
))
}
*/ */
/* /*
async fn collect_multipart(
mut stream: warp::filters::multipart::FormData,
) -> Result<Vec<(Option<String>, Option<String>, Vec<u8>)>, warp::Error> {
let mut content: Vec<(Option<String>, Option<String>, Vec<u8>)> = Vec::new();
while let Some(part) = stream.next().await {
match part {
Ok(part) => content.push(collect_content(part).await.unwrap()),
Err(err) => return Err(err),
}
}
Ok(content)
}
*/ */
/* /*
async fn handle_upload(
form: warp::filters::multipart::FormData,
app: App,
) -> warp::http::Result<warp::http::Response<String>> {
let files = collect_multipart(form).await;
match files {
Ok(files) => {
for (_, filename, content) in files {
match filename {
Some(filename) => {
app.add_file(filename, content).unwrap();
}
None => {
return warp::http::Response::builder()
.status(StatusCode::BAD_REQUEST)
.body("".to_owned())
}
}
}
}
Err(_err) => {
return warp::http::Response::builder()
.status(StatusCode::BAD_REQUEST)
.body("".to_owned())
}
}
// println!("file length: {:?}", files.map(|f| f.len()));
warp::http::Response::builder()
.header("location", "/")
.status(StatusCode::SEE_OTHER)
.body("".to_owned())
}
*/ */
#[derive(Clone)] #[derive(Clone)]
@ -159,26 +96,25 @@ fn with_app(app: App) -> impl Filter<Extract = (App,), Error = Infallible> + Clo
warp::any().map(move || app.clone()) warp::any().map(move || app.clone())
} }
fn parse_cookies(cookie_str: &str) -> Result<HashMap<String, String>, cookie::ParseError> {
Cookie::split_parse(cookie_str)
.map(|c| c.map(|c| (c.name().to_owned(), c.value().to_owned())))
.collect::<Result<HashMap<String, String>, cookie::ParseError>>()
}
fn get_session_token(cookies: HashMap<String, String>) -> Option<SessionToken> {
cookies
.get("session")
.cloned()
.and_then(|session| Some(SessionToken::from(session)))
}
fn maybe_with_session() -> impl Filter<Extract = (Option<SessionToken>,), Error = Rejection> + Copy fn maybe_with_session() -> impl Filter<Extract = (Option<SessionToken>,), Error = Rejection> + Copy
{ {
warp::any() warp::any()
.and(warp::header::optional::<String>("cookie")) .and(warp::header::optional::<String>("cookie"))
.map(|cookies| match cookies { .map(|cookie_str: Option<String>| match cookie_str {
Some(cookies) => { Some(cookie_str) => parse_cookies(&cookie_str).ok().and_then(get_session_token),
let c = Cookie::split_parse(cookies)
.collect::<Result<Vec<Cookie>, cookie::ParseError>>();
match c {
Ok(cookies) => {
for c in cookies {
if c.name() == "session" {
return Some(SessionToken::from(c.value()));
}
}
None
}
Err(_) => None,
}
}
None => None, None => None,
}) })
} }
@ -186,7 +122,12 @@ fn maybe_with_session() -> impl Filter<Extract = (Option<SessionToken>,), Error
fn with_session() -> impl Filter<Extract = (SessionToken,), Error = Rejection> + Copy { fn with_session() -> impl Filter<Extract = (SessionToken,), Error = Rejection> + Copy {
warp::any() warp::any()
.and(warp::header::<String>("cookie")) .and(warp::header::<String>("cookie"))
.map(|token: String| SessionToken::from(token)) .and_then(|cookie_str: String| async move {
match parse_cookies(&cookie_str).ok().and_then(get_session_token) {
Some(session_token) => Ok(session_token),
None => Err(warp::reject()),
}
})
} }
#[tokio::main] #[tokio::main]
@ -200,13 +141,6 @@ pub async fn main() {
let app = App::new(authdb, store); let app = App::new(authdb, store);
/*
let with_app = {
let app = app.clone();
warp::any().map(move || app.clone())
};
*/
let log = warp::log("file_service"); let log = warp::log("file_service");
let root = warp::path!() let root = warp::path!()
.and(warp::get()) .and(warp::get())
@ -220,10 +154,11 @@ pub async fn main() {
.and(warp::filters::body::form()) .and(warp::filters::body::form())
.then(handle_auth); .then(handle_auth);
let upload_handler = warp::path!("upload") let upload_via_form = warp::path!("upload")
.and(warp::post()) .and(warp::post())
.and(with_app(app.clone())) .and(with_app(app.clone()))
.and(with_session()) .and(with_session())
.and(warp::multipart::form())
.then(handle_upload); .then(handle_upload);
let thumbnail = warp::path!(String / "tn") let thumbnail = warp::path!(String / "tn")
@ -239,13 +174,11 @@ pub async fn main() {
.then(move |id, old_etags, app: App| file(app, id, old_etags)); .then(move |id, old_etags, app: App| file(app, id, old_etags));
let server = warp::serve( let server = warp::serve(
root.or(auth).with(log), /*
root.or(auth) root.or(auth)
.or(upload_via_form)
.or(thumbnail) .or(thumbnail)
.or(file) .or(file)
.or(upload_handler)
.with(log), .with(log),
*/
); );
server server