]>
git.nega.tv - josh/git-shell-multiplex/blob - src/main.rs
db2d9cbc1c3dbf763a6204c8fc0a2b39f6c7e30f
1 //! git-shell-multiplex
3 //! An implementation of `git-shell` which allows multiple users to access git
4 //! repositories through a single linux user account. It works by using the
5 //! `command` parameter of `authorized_keys` entries to associate a username
6 //! with a public key, then correlating that with the permissions of the git
9 //! There's no support for an interactive shell, and only the following git
10 //! commands are available.
11 //! * `git-receive-pack`
12 //! * `git-upload-pack`
13 //! * `git-upload-archive`
18 //! `PUBLIC_ACCESS_FILE`
20 //! `git-shell-multiplex` looks for the file `PUBLIC_ACCESS_FILE` (default:
21 //! `git-daemon-export-ok`) within the git repository, if found all users will be
22 //! granted read-access to the repository.
24 //! `CONTRIBUTORS_FILE`
26 //! `git-shell-multiplex` looks for the file `PUBLIC_ACCESS_FILE` (default:
27 //! `git-shell-multiplex-contributors`) within the git repository. The file is
28 //! expected to contain a newline-separated list of additional contributor
29 //! usernames. These users will be granted write access to the repository, in
30 //! addition to the repository owner.
32 //! Example `authorized_keys` file
36 //! command="/home/git/git-shell-multiplex josh",restrict <pubkey>
37 //! command="/home/git/git-shell-multiplex sarah",restrict <pubkey>
40 //! Accepts paths of the form `<user>/<repo>.git` only.
43 use std
::io
::{BufRead
as _
, BufReader
, Lines
};
44 use std
::path
::{Path
, PathBuf
};
48 NotAShell
{ username
: String
},
52 /// Repositories should be found at `<GIT_DIR>/<user>/<repo>`. Note the lack of `.git`.
53 const GIT_DIR
: &str = "/var/git/";
55 /// If a file with this name is present in a git repository, read-access will be
56 /// granted to anybody with ssh access.
57 const PUBLIC_ACCESS_FILE
: &str = "git-daemon-export-ok";
59 /// This file, if it exists, should contain a newline-separated list of additional
60 /// contributor usernames. These users will be granted write access to the
63 /// If the file does not exist, only the owner of the repository will be able to
64 /// write to the repository.
65 const CONTRIBUTORS_FILE
: &str = "git-shell-multiplex-contributors";
67 fn read_lines
<P
>(path
: P
) -> std
::io
::Result
<Lines
<BufReader
<File
>>>
71 let file
= File
::open(path
)?
;
72 Ok(BufReader
::new(file
).lines())
75 fn git_shell() -> Result
<(), Error
> {
76 let mut args
= std
::env
::args();
78 // First arg is the process name.
79 let _
= args
.next().ok_or(Error
::Usage
)?
;
80 let username
= args
.next().ok_or(Error
::Usage
)?
;
82 // Usage error if we pass extra arguments.
83 if args
.next().is_some() {
84 return Err(Error
::Usage
);
87 // If sshd hasn't set the original command, some goober is trying to use the
88 // git account as a shell.
89 let Some(ssh_original_command
) = std
::env
::var_os("SSH_ORIGINAL_COMMAND") else {
90 return Err(Error
::NotAShell
{ username
});
93 // If the incoming command isn't valid unicode.
94 let ssh_original_command
= ssh_original_command
.to_str().ok_or(Error
::NoPermission
)?
;
96 // The `SSH_ORIGINAL_COMMAND` is escaped, so we need to undo that.
100 let mut words
= shlex
::Shlex
::new(ssh_original_command
);
101 command
= words
.next().ok_or(Error
::NoPermission
)?
;
102 path
= words
.next().ok_or(Error
::NoPermission
)?
;
103 if words
.next().is_some() {
104 return Err(Error
::NoPermission
);
108 // We want to support both `git-blah-blah` and `git blah-blah`.
109 let command
= if let Some(rest
) = command
.strip_prefix("git-") {
111 } else if let Some(rest
) = command
.strip_prefix("git ") {
114 return Err(Error
::NoPermission
);
117 // We exclusively support paths that look like `<user>/<repo>.git`.
118 let (repo_owner
, repo_name
) = path
.split_once('
/'
).ok_or(Error
::NoPermission
)?
;
119 let repo_name
= repo_name
.strip_suffix(".git").ok_or(Error
::NoPermission
)?
;
121 // Hopefully this is enough...
122 const UNSAFE_CHARS
: &[char] = &['
\\'
, '
.'
, '
/'
, '
:'
, '?'
];
123 if repo_owner
.contains(UNSAFE_CHARS
) || repo_name
.contains(UNSAFE_CHARS
) {
124 return Err(Error
::NoPermission
);
127 // On the filesystem we don't have a `.git` suffix for repositories.
128 let repo_path
= PathBuf
::from(format!("{GIT_DIR}/{repo_owner}/{repo_name}"));
130 // Is this actually a git repo?
131 let has_head_file
= matches!(std
::fs
::exists(repo_path
.join("HEAD")), Ok(true));
133 return Err(Error
::NoPermission
);
136 // If the repo contains a file named `git-daemon-export-ok` we allow public access.
137 let is_repo_public
= matches!(
138 std
::fs
::exists(repo_path
.join(PUBLIC_ACCESS_FILE
)),
142 let is_user_a_contributor
=
143 if let Ok(contributors
) = read_lines(repo_path
.join(CONTRIBUTORS_FILE
)) {
145 .map_while(Result
::ok
)
146 .any(|contributor_username
| contributor_username
== username
)
151 // Check the permissions...
152 let is_user_the_repo_owner
= repo_owner
== username
;
153 let write_access
= is_user_the_repo_owner
|| is_user_a_contributor
;
154 let read_access
= is_user_the_repo_owner
|| is_user_a_contributor
|| is_repo_public
;
156 let has_permission
= match command
{
157 "receive-pack" => write_access
,
158 "upload-pack" | "upload-archive" => read_access
,
160 // Spooky unknown commands ~~~
165 return Err(Error
::NoPermission
);
168 // Finally we can actually run the git command.
169 let program
= format!("git-{command}");
170 let mut child
= std
::process
::Command
::new(program
)
173 .map_err(|_
| Error
::NoPermission
)?
;
174 let exit_status
= child
.wait().map_err(|_
| Error
::NoPermission
)?
;
176 if exit_status
.success() {
179 Err(Error
::NoPermission
)
183 pub fn main() -> std
::process
::ExitCode
{
185 Ok(_
) => std
::process
::ExitCode
::SUCCESS
,
186 Err(error
) => match error
{
188 eprintln!("usage: `git-shell-multiplex <username>");
189 std
::process
::ExitCode
::FAILURE
191 Error
::NotAShell
{ username
} => {
192 eprintln!("Hello {username}! This isn't the shell you're looking for.");
193 std
::process
::ExitCode
::SUCCESS
195 Error
::NoPermission
=> std
::process
::ExitCode
::FAILURE
,