diff options
-rw-r--r-- | src/get_routes.rs | 37 | ||||
-rw-r--r-- | src/main.rs | 51 | ||||
-rw-r--r-- | src/post_routes.rs | 3 | ||||
-rw-r--r-- | src/static/style.css | 2 | ||||
-rw-r--r-- | src/static/style.css.sha | 2 |
5 files changed, 75 insertions, 20 deletions
diff --git a/src/get_routes.rs b/src/get_routes.rs index e07050d..57d7e56 100644 --- a/src/get_routes.rs +++ b/src/get_routes.rs @@ -25,6 +25,7 @@ use crate::Context; use crate::Error; use crate::HyperResponse; use crate::Page; +use crate::RenderMode; use crate::Response; pub(crate) fn get_blob<C: Controller>( @@ -40,7 +41,8 @@ pub(crate) fn get_blob<C: Controller>( "upload" => Ok(forms::upload_form(true, controller, &ctx, parts).into()), "log" => log_blob(entr, params, controller, ctx, parts), "diff" => diff_blob(entr, params, controller, ctx, parts), - "raw" => raw_blob(entr, params, controller, ctx, parts), + "raw" => raw_blob(entr, params, controller, ctx, parts).map(|r| r.into()), + "run" => run_blob(entr, params, controller, ctx, parts).map(|r| r.into()), "move" => move_blob(entr, params, controller, ctx, parts), "remove" => remove_blob(entr, params, controller, ctx, parts), _ => Err(Error::BadRequest("unknown action".into())), @@ -81,7 +83,7 @@ fn view_blob<C: Controller>( match from_utf8(blob.content()) { Ok(text) => { if let Some(renderer) = get_renderer(&ctx.path) { - renderer(text, &mut page); + renderer(text, &mut page, RenderMode::View); } else { page.body .push_str(&format!("<pre>{}</pre>", html_escape(text))); @@ -314,7 +316,7 @@ fn raw_blob<C: Controller>( _controller: &C, ctx: Context, parts: &Parts, -) -> Result<Response, Error> { +) -> Result<HyperResponse, Error> { if let Some(etag) = parts .headers .get(header::IF_NONE_MATCH) @@ -348,7 +350,34 @@ fn raw_blob<C: Controller>( .insert(header::CONTENT_TYPE, mime.to_string().parse().unwrap()); } } - Ok(resp.into()) + Ok(resp) +} + +fn run_blob<C: Controller>( + entr: TreeEntry, + params: ActionParam, + controller: &C, + ctx: Context, + parts: &Parts, +) -> Result<HyperResponse, Error> { + if ctx.path.extension().unwrap().to_str() != Some("html") { + return Err(Error::BadRequest( + "run action only available for .html files".into(), + )); + } + raw_blob(entr, params, controller, ctx, parts).map(|mut r| { + r.headers_mut() + .insert(header::CONTENT_TYPE, "text/html".parse().unwrap()); + // We want users to be able to view .html applications of other users + // without having to worry about the application accessing their private + // files. So we set the CSP: sandbox header which makes browsers treat + // the page as a unique origin as per the same-origin policy. + r.headers_mut().insert( + header::CONTENT_SECURITY_POLICY, + "sandbox allow-scripts;".parse().unwrap(), + ); + r + }) } fn move_blob<C: Controller>( diff --git a/src/main.rs b/src/main.rs index a5b7ce0..58431a2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -265,17 +265,18 @@ async fn service<C: Controller>( }); // we rely on CSP to thwart XSS attacks, all modern browsers support it - resp.headers_mut().insert( - header::CONTENT_SECURITY_POLICY, - format!( - "default-src 'self'; frame-src {}; script-src {}; style-src {}", - frame_csp, - script_csp, - include_str!("static/style.css.sha"), - ) - .parse() - .unwrap(), - ); + resp.headers_mut() + .entry(header::CONTENT_SECURITY_POLICY) + .or_insert_with(|| { + format!( + "default-src 'self'; frame-src {}; script-src {}; style-src {}", + frame_csp, + script_csp, + include_str!("static/style.css.sha"), + ) + .parse() + .unwrap() + }); Ok(resp) } @@ -526,16 +527,40 @@ impl Context { } } -fn render_markdown(input: &str, page: &mut Page) { +#[derive(PartialEq)] +enum RenderMode { + View, + Preview, +} + +fn render_markdown(input: &str, page: &mut Page, _mode: RenderMode) { let parser = Parser::new_ext(input, Options::all()); page.body.push_str("<div class=markdown-output>"); html::push_html(&mut page.body, parser); page.body.push_str("</div>"); } -fn get_renderer(path: &Path) -> Option<fn(&str, &mut Page)> { +fn embed_html_as_iframe(input: &str, page: &mut Page, mode: RenderMode) { + if mode == RenderMode::View { + page.body.push_str("<iframe src='?action=run'></iframe>"); + page.frame_src = Some("'self'"); + } else { + page.body + .push_str("<div class=note>Note that JavaScript does not work in the preview.</div>"); + // sandbox=allow-scripts wouldn't work because the strict parent page CSP still applies + + // The sandbox attribute makes browsers treat the embedded page as a unique origin. + page.body.push_str(&format!( + "<iframe srcdoc='{}' sandbox></iframe>", + html_escape(input) + )); + } +} + +fn get_renderer(path: &Path) -> Option<fn(&str, &mut Page, RenderMode)> { match path.extension().map(|e| e.to_str().unwrap()) { Some("md") => Some(render_markdown), + Some("html") => Some(embed_html_as_iframe), _ => None, } } diff --git a/src/post_routes.rs b/src/post_routes.rs index 392a024..7588abc 100644 --- a/src/post_routes.rs +++ b/src/post_routes.rs @@ -20,6 +20,7 @@ use crate::get_renderer; use crate::ActionParam; use crate::Args; use crate::Context; +use crate::RenderMode; use crate::Response; use crate::{controller::Controller, Error}; @@ -363,6 +364,6 @@ async fn preview_edit<C: Controller>( let form: EditForm = body.into_form().await?; let new_text = form.text.replace("\r\n", "\n"); let mut page = edit_text_form(&form, None, controller, &ctx, parts); - get_renderer(&ctx.path).unwrap()(&new_text, &mut page); + get_renderer(&ctx.path).unwrap()(&new_text, &mut page, RenderMode::Preview); Ok(page.into()) } diff --git a/src/static/style.css b/src/static/style.css index 8ee788d..af60760 100644 --- a/src/static/style.css +++ b/src/static/style.css @@ -23,7 +23,7 @@ h1 { flex-grow: 1; } -textarea { +textarea, iframe { flex-grow: 1; } diff --git a/src/static/style.css.sha b/src/static/style.css.sha index 774a743..ed20f8e 100644 --- a/src/static/style.css.sha +++ b/src/static/style.css.sha @@ -1 +1 @@ -'sha256-9xIJ8fFnUyBYBKBAR6JWlmi39sUqyIqzUt1PnrkDE/c='
\ No newline at end of file +'sha256-7wOMnVUwWmDEG4Yz3cqmo7y38q0qzzpSB9TiwkKMHmg='
\ No newline at end of file |