aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Fischer <martin@push-f.com>2021-06-23 22:46:31 +0200
committerMartin Fischer <martin@push-f.com>2021-06-23 22:50:55 +0200
commit43b4b8693890a85f24eb358bc5545232ebf8e796 (patch)
treec83bbcacae18b034037399c197e1976346c40eb9
parentca074febae4cd56ad5443c110a15662fa110dd81 (diff)
make single-user mode operate on HEAD branch
-rw-r--r--README.md18
-rw-r--r--src/controller.rs108
-rw-r--r--src/get_routes.rs6
-rw-r--r--src/main.rs85
-rw-r--r--src/post_routes.rs8
5 files changed, 133 insertions, 92 deletions
diff --git a/README.md b/README.md
index f78384b..34b4dce 100644
--- a/README.md
+++ b/README.md
@@ -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())