diff options
author | Martin Fischer <martin@push-f.com> | 2021-06-23 22:46:31 +0200 |
---|---|---|
committer | Martin Fischer <martin@push-f.com> | 2021-06-23 22:50:55 +0200 |
commit | 43b4b8693890a85f24eb358bc5545232ebf8e796 (patch) | |
tree | c83bbcacae18b034037399c197e1976346c40eb9 | |
parent | ca074febae4cd56ad5443c110a15662fa110dd81 (diff) |
make single-user mode operate on HEAD branch
-rw-r--r-- | README.md | 18 | ||||
-rw-r--r-- | src/controller.rs | 108 | ||||
-rw-r--r-- | src/get_routes.rs | 6 | ||||
-rw-r--r-- | src/main.rs | 85 | ||||
-rw-r--r-- | src/post_routes.rs | 8 |
5 files changed, 133 insertions, 92 deletions
@@ -22,16 +22,17 @@ $ gitpad Listening on http://127.0.0.1:8000 ``` -Files are served under `/~{branch}/{path}`, so for example `/~hello/world.md` -refers to the `world.md` file in the `hello` branch. By default GitPad is in -single-user mode, letting the user view and edit all branches (as well as create -new branches). +By default GitPad is in single-user mode, serving the branch pointed to by `HEAD`. ## Multi-user mode Multi-user mode requires you to set up a reverse-proxy that authenticates users -and sets the `Username` header. The simplest authentication mechanism is HTTP -Basic Auth. With NGINX a reverse-proxy could be configured as follows: +and sets the `Username` header. Every user gets their own private Git branch, +named exactly like their username and served under `/~{username}`. Users can +share files/directories with other users by creating a `.shares.txt` file. + +The simplest authentication mechanism is HTTP Basic Auth. With NGINX a +reverse-proxy could be configured as follows: ```nginx server { @@ -60,11 +61,6 @@ refer to the [NGINX documentation](https://docs.nginx.com/nginx/admin-guide/secu Once you have set this up start GitPad in multi-user mode by running it with the `-m` flag. -In multi-user mode every user has their own Git branch, named exactly like their -username. Your own branch is private by default, other users cannot access your -files. Users can however share files/directories with other users by creating a -`.shares.txt` file. - ## Configuring committer identities In single-user mode GitPad just uses the diff --git a/src/controller.rs b/src/controller.rs index 1f66b44..5dc2a47 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -3,10 +3,10 @@ use std::path::Path; use std::str::from_utf8; use std::sync::RwLock; -use git2::ObjectType; use git2::Repository; use git2::Signature; use git2::Tree; +use git2::{BranchType, ObjectType}; use hyper::http::request::Parts; use serde::Deserialize; use sputnik::html_escape; @@ -19,8 +19,13 @@ use crate::Page; use crate::Response; pub trait Controller { - /// Allows the controller to abort if the request is invalid. - fn before_route(&self, parts: &Parts) -> Result<(), Error>; + fn parse_url_path<'a>(&'a self, url_path: &'a str) -> Result<(Branch, &'a str), Error>; + + /// Builds a URL path from a given Git branch and file path. + fn build_url_path<'a>(&self, branch: &Branch, path: &'a str) -> String; + + /// Allows the controller to intercept the request handling. + fn before_route(&self, parts: &Parts, repo: &Repository) -> Option<Result<Response, Error>>; /// Returns some HTML info to display for the current page. fn user_info_html(&self, parts: &Parts) -> Option<String> { @@ -48,7 +53,7 @@ pub trait Controller { None } - fn before_return_tree_page(&self, page: &mut Page, tree: Option<Tree>, context: &Context) {} + fn before_return_tree_page(&self, page: &mut Page, tree: Option<Tree>, context: &Context); /// Executed before writing a file. Return an error to abort the writing process. fn before_write(&self, text: &str, context: &mut Context) -> Result<(), String> { @@ -64,21 +69,29 @@ pub trait Controller { } } -pub struct SoloController; +pub struct SoloController(pub Branch); const USERNAME_HEADER: &str = "Username"; impl Controller for SoloController { - fn before_route(&self, parts: &Parts) -> Result<(), Error> { + fn parse_url_path<'a>(&'a self, url_path: &'a str) -> Result<(Branch, &'a str), Error> { + Ok((self.0.clone(), url_path)) + } + + fn build_url_path<'a>(&self, branch: &Branch, path: &'a str) -> String { + path.to_owned() + } + + fn before_route(&self, parts: &Parts, repo: &Repository) -> Option<Result<Response, Error>> { if parts.headers.contains_key(USERNAME_HEADER) { - return Err(Error::BadRequest(format!( + return Some(Err(Error::BadRequest(format!( "unexpected header {} (only \ allowed in multi-user mode), aborting to prevent accidental \ information leakage", USERNAME_HEADER - ))); + )))); } - Ok(()) + None } fn signature(&self, repo: &Repository, parts: &Parts) -> Result<Signature, Error> { @@ -97,6 +110,12 @@ impl Controller for SoloController { true } + fn before_return_tree_page(&self, page: &mut Page, tree: Option<Tree>, context: &Context) { + if tree.map(|t| t.len()).unwrap_or_default() == 0 { + page.body.push_str("<p>create files by editing the URL, e.g. <a href='/hello-world.md'>/hello-world.md</a></p>"); + } + } + fn before_write(&self, text: &str, ctx: &mut Context) -> Result<(), String> { if let Some(ext) = ctx.path.extension().and_then(|e| e.to_str()) { validate_formats(text, ext)?; @@ -250,17 +269,80 @@ fn validate_formats(text: &str, extension: &str) -> Result<(), String> { Ok(()) } +fn multi_user_startpage( + controller: &MultiUserController, + parts: &Parts, + repo: &Repository, +) -> Result<Response, Error> { + // TODO: add domain name to title? + let mut page = Page { + title: "GitPad".into(), + controller, + parts: &parts, + body: String::new(), + header: None, + }; + + let branches: Vec<_> = repo.branches(Some(BranchType::Local))?.collect(); + + page.body.push_str("This GitPad instance has "); + + if branches.is_empty() { + page.body.push_str("no branches yet."); + } else { + page.body.push_str("the following branches:"); + page.body.push_str("<ul>"); + for (branch, _) in repo.branches(Some(BranchType::Local))?.flatten() { + page.body.push_str(&format!( + "<li><a href='~{0}/'>~{0}</a></li>", + html_escape(branch.name()?.unwrap()) + )); + } + page.body.push_str("</ul>"); + } + + Ok(page.into()) +} + impl Controller for MultiUserController { - fn before_route(&self, parts: &Parts) -> Result<(), Error> { + fn parse_url_path<'a>(&'a self, url_path: &'a str) -> Result<(Branch, &'a str), Error> { + let mut iter = url_path.splitn(3, '/'); + iter.next(); + let rev = iter.next().unwrap(); + if !rev.starts_with('~') { + return Err(Error::NotFound( + "branch name must be prefixed a tilde (~)".into(), + )); + } + let rev = &rev[1..]; + if rev.trim().is_empty() { + return Err(Error::NotFound("invalid branch name".into())); + } + let rev = Branch(rev.to_owned()); + let unsanitized_path = match iter.next() { + Some(value) => value, + None => return Err(Error::MissingTrailingSlash(url_path.to_string())), + }; + Ok((rev, unsanitized_path)) + } + + fn build_url_path<'a>(&self, branch: &Branch, path: &'a str) -> String { + format!("/~{}/{}", branch.0, path) + } + + fn before_route(&self, parts: &Parts, repo: &Repository) -> Option<Result<Response, Error>> { if !parts.headers.contains_key(USERNAME_HEADER) { - return Err(Error::BadRequest(format!( + return Some(Err(Error::BadRequest(format!( "expected header {} because of multi-user mode \ (this shouldn't be happening because a reverse-proxy \ should be used to set this header)", USERNAME_HEADER - ))); + )))); } - Ok(()) + if parts.uri.path() == "/" { + return Some(multi_user_startpage(&self, parts, repo)); + } + None } fn user_info_html(&self, parts: &Parts) -> Option<String> { diff --git a/src/get_routes.rs b/src/get_routes.rs index 433e676..01332e1 100644 --- a/src/get_routes.rs +++ b/src/get_routes.rs @@ -329,8 +329,10 @@ pub fn view_tree<C: Controller>( }; page.body.push_str("<ul>"); - page.body - .push_str("<li><a href='..' title='go to parent directory'>../</a></li>"); + if ctx.parts.uri.path() != "/" { + page.body + .push_str("<li><a href='..' title='go to parent directory'>../</a></li>"); + } if let Ok(tree) = &tree { let mut entries: Vec<_> = tree.iter().collect(); diff --git a/src/main.rs b/src/main.rs index d521087..30f018b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,5 @@ use clap::Clap; use controller::Controller; -use git2::BranchType; use git2::Commit; use git2::ObjectType; use git2::Oid; @@ -103,7 +102,19 @@ async fn main() { if args.multiuser { serve(MultiUserController::new(&repo), args).await; } else { - serve(SoloController, args).await; + serve( + SoloController(Branch( + repo.find_reference("HEAD") + .unwrap() + .symbolic_target() + .unwrap() + .strip_prefix("refs/heads/") + .unwrap() + .to_owned(), + )), + args, + ) + .await; } } @@ -195,7 +206,7 @@ pub enum Error { // TODO: use Redirect instead /// Missing trailing slash. - MissingTrailingSlash(Parts), + MissingTrailingSlash(String), } impl From<Utf8Error> for Error { @@ -223,10 +234,10 @@ async fn service<C: Controller>( Error::Forbidden(msg) => (403, msg), Error::NotFound(msg) => (404, msg), Error::Internal(msg) => (500, msg), - Error::MissingTrailingSlash(parts) => { + Error::MissingTrailingSlash(path) => { return Builder::new() .status(StatusCode::FOUND) - .header("location", format!("{}/", parts.uri.path())) + .header("location", format!("{}/", path)) .body("redirecting".into()) .unwrap(); } @@ -321,11 +332,6 @@ impl From<git2::Error> for Error { } } -/// Builds a URL path from a given Git revision and filepath. -fn build_url_path(rev: &Branch, path: &str) -> String { - format!("/~{}/{}", rev.0, path) -} - #[derive(Eq, PartialEq, Hash, Clone)] pub struct Branch(String); @@ -341,7 +347,6 @@ async fn build_response<C: Controller>( parts: Parts, body: Body, ) -> Result<Response, Error> { - controller.before_route(&parts)?; let unsanitized_path = percent_decode_str(parts.uri.path()) .decode_utf8() .map_err(|_| Error::BadRequest("failed to percent-decode path as UTF-8".into()))? @@ -349,58 +354,11 @@ async fn build_response<C: Controller>( let repo = Repository::open_bare(env::current_dir().unwrap()).unwrap(); - if parts.uri.path() == "/" { - // TODO: add domain name to title? - let mut page = Page { - title: "GitPad".into(), - controller, - parts: &parts, - body: String::new(), - header: None, - }; - - let branches: Vec<_> = repo.branches(Some(BranchType::Local))?.collect(); - - page.body.push_str("This GitPad instance has "); - - if branches.is_empty() { - page.body.push_str("no branches yet."); - - if !args.multiuser { - page.body.push_str("<p>Start by creating for example <a href='/~main/todo.md'>/~main/todo.md</a>.</p>"); - } - } else { - page.body.push_str("the following branches:"); - page.body.push_str("<ul>"); - for (branch, _) in repo.branches(Some(BranchType::Local))?.flatten() { - page.body.push_str(&format!( - "<li><a href='~{0}/'>~{0}</a></li>", - html_escape(branch.name()?.unwrap()) - )); - } - page.body.push_str("</ul>"); - } - - return Ok(page.into()); + if let Some(resp) = controller.before_route(&parts, &repo) { + return resp; } - let mut iter = unsanitized_path.splitn(3, '/'); - iter.next(); - let rev = iter.next().unwrap(); - if !rev.starts_with('~') { - return Err(Error::NotFound( - "branch name must be prefixed a tilde (~)".into(), - )); - } - let rev = &rev[1..]; - if rev.trim().is_empty() { - return Err(Error::NotFound("invalid branch name".into())); - } - let rev = Branch(rev.to_owned()); - let unsanitized_path = match iter.next() { - Some(value) => value, - None => return Err(Error::MissingTrailingSlash(parts)), - }; + let (rev, unsanitized_path) = controller.parse_url_path(&unsanitized_path)?; let mut comps = Vec::new(); @@ -478,7 +436,8 @@ async fn build_response<C: Controller>( .status(StatusCode::FOUND) .header( "location", - build_url_path(&ctx.branch, unsanitized_path.trim_end_matches('/')), + controller + .build_url_path(&ctx.branch, unsanitized_path.trim_end_matches('/')), ) .body("redirecting".into()) .unwrap()); @@ -488,7 +447,7 @@ async fn build_response<C: Controller>( tree = ctx.repo.find_tree(entr.id()); if !unsanitized_path.ends_with('/') { - return Err(Error::MissingTrailingSlash(ctx.parts)); + return Err(Error::MissingTrailingSlash(ctx.parts.uri.path().to_owned())); } } diff --git a/src/post_routes.rs b/src/post_routes.rs index 0d4631c..311ee57 100644 --- a/src/post_routes.rs +++ b/src/post_routes.rs @@ -10,7 +10,6 @@ use sputnik::hyper_body::SputnikBody; use std::path::Path; use std::str::from_utf8; -use crate::build_url_path; use crate::diff::diff; use crate::forms::edit_text_form; use crate::forms::move_form; @@ -268,7 +267,10 @@ async fn move_entry<C: Controller>( Ok(Builder::new() .status(StatusCode::FOUND) - .header("location", build_url_path(&ctx.branch, &data.dest)) + .header( + "location", + controller.build_url_path(&ctx.branch, &data.dest), + ) .body("redirecting".into()) .unwrap()) } @@ -308,7 +310,7 @@ async fn remove_entry<C: Controller>( .status(StatusCode::FOUND) .header( "location", - build_url_path(&ctx.branch, ctx.path.parent().unwrap().to_str().unwrap()), + controller.build_url_path(&ctx.branch, ctx.path.parent().unwrap().to_str().unwrap()), ) .body("redirecting".into()) .unwrap()) |