Skip to content

Commit ae893e0

Browse files
committed
Merge branch 'gb/shell-ext'
* gb/shell-ext: shell: Display errors from improperly-formatted command lines shell: Rewrite documentation and improve error message Add sample commands for git-shell Add interactive mode to git-shell for user-friendliness Allow creation of arbitrary git-shell commands
2 parents 02ef0ed + 9f29fe9 commit ae893e0

5 files changed

Lines changed: 186 additions & 21 deletions

File tree

Documentation/git-shell.txt

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,30 @@ git-shell(1)
33

44
NAME
55
----
6-
git-shell - Restricted login shell for GIT-only SSH access
6+
git-shell - Restricted login shell for Git-only SSH access
77

88

99
SYNOPSIS
1010
--------
11-
'$(git --exec-path)/git-shell' -c <command> <argument>
11+
'git shell' [-c <command> <argument>]
1212

1313
DESCRIPTION
1414
-----------
15-
This is meant to be used as a login shell for SSH accounts you want
16-
to restrict to GIT pull/push access only. It permits execution only
17-
of server-side GIT commands implementing the pull/push functionality.
18-
The commands can be executed only by the '-c' option; the shell is not
19-
interactive.
20-
21-
Currently, only four commands are permitted to be called, 'git-receive-pack'
22-
'git-upload-pack' and 'git-upload-archive' with a single required argument, or
23-
'cvs server' (to invoke 'git-cvsserver').
15+
16+
A login shell for SSH accounts to provide restricted Git access. When
17+
'-c' is given, the program executes <command> non-interactively;
18+
<command> can be one of 'git receive-pack', 'git upload-pack', 'git
19+
upload-archive', 'cvs server', or a command in COMMAND_DIR. The shell
20+
is started in interactive mode when no arguments are given; in this
21+
case, COMMAND_DIR must exist, and any of the executables in it can be
22+
invoked.
23+
24+
'cvs server' is a special command which executes git-cvsserver.
25+
26+
COMMAND_DIR is the path "$HOME/git-shell-commands". The user must have
27+
read and execute permissions to the directory in order to execute the
28+
programs in it. The programs are executed with a cwd of $HOME, and
29+
<argument> is parsed as a command-line string.
2430

2531
Author
2632
------

contrib/git-shell-commands/README

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Sample programs callable through git-shell. Place a directory named
2+
'git-shell-commands' in the home directory of a user whose shell is
3+
git-shell. Then anyone logging in as that user will be able to run
4+
executables in the 'git-shell-commands' directory.
5+
6+
Provided commands:
7+
8+
help: Prints out the names of available commands. When run
9+
interactively, git-shell will automatically run 'help' on startup,
10+
provided it exists.
11+
12+
list: Displays any bare repository whose name ends with ".git" under
13+
user's home directory. No other git repositories are visible,
14+
although they might be clonable through git-shell. 'list' is designed
15+
to minimize the number of calls to git that must be made in finding
16+
available repositories; if your setup has additional repositories that
17+
should be user-discoverable, you may wish to modify 'list'
18+
accordingly.

contrib/git-shell-commands/help

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/bin/sh
2+
3+
if tty -s
4+
then
5+
echo "Run 'help' for help, or 'exit' to leave. Available commands:"
6+
else
7+
echo "Run 'help' for help. Available commands:"
8+
fi
9+
10+
cd "$(dirname "$0")"
11+
12+
for cmd in *
13+
do
14+
case "$cmd" in
15+
help) ;;
16+
*) [ -f "$cmd" ] && [ -x "$cmd" ] && echo "$cmd" ;;
17+
esac
18+
done

contrib/git-shell-commands/list

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/bin/sh
2+
3+
print_if_bare_repo='
4+
if "$(git --git-dir="$1" rev-parse --is-bare-repository)" = true
5+
then
6+
printf "%s\n" "${1#./}"
7+
fi
8+
'
9+
10+
find -type d -name "*.git" -exec sh -c "$print_if_bare_repo" -- \{} \; -prune 2>/dev/null

shell.c

Lines changed: 123 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
#include "quote.h"
33
#include "exec_cmd.h"
44
#include "strbuf.h"
5+
#include "run-command.h"
6+
7+
#define COMMAND_DIR "git-shell-commands"
8+
#define HELP_COMMAND COMMAND_DIR "/help"
59

610
static int do_generic_cmd(const char *me, char *arg)
711
{
@@ -33,6 +37,86 @@ static int do_cvs_cmd(const char *me, char *arg)
3337
return execv_git_cmd(cvsserver_argv);
3438
}
3539

40+
static int is_valid_cmd_name(const char *cmd)
41+
{
42+
/* Test command contains no . or / characters */
43+
return cmd[strcspn(cmd, "./")] == '\0';
44+
}
45+
46+
static char *make_cmd(const char *prog)
47+
{
48+
char *prefix = xmalloc((strlen(prog) + strlen(COMMAND_DIR) + 2));
49+
strcpy(prefix, COMMAND_DIR);
50+
strcat(prefix, "/");
51+
strcat(prefix, prog);
52+
return prefix;
53+
}
54+
55+
static void cd_to_homedir(void)
56+
{
57+
const char *home = getenv("HOME");
58+
if (!home)
59+
die("could not determine user's home directory; HOME is unset");
60+
if (chdir(home) == -1)
61+
die("could not chdir to user's home directory");
62+
}
63+
64+
static void run_shell(void)
65+
{
66+
int done = 0;
67+
static const char *help_argv[] = { HELP_COMMAND, NULL };
68+
/* Print help if enabled */
69+
run_command_v_opt(help_argv, RUN_SILENT_EXEC_FAILURE);
70+
71+
do {
72+
struct strbuf line = STRBUF_INIT;
73+
const char *prog;
74+
char *full_cmd;
75+
char *rawargs;
76+
char *split_args;
77+
const char **argv;
78+
int code;
79+
int count;
80+
81+
fprintf(stderr, "git> ");
82+
if (strbuf_getline(&line, stdin, '\n') == EOF) {
83+
fprintf(stderr, "\n");
84+
strbuf_release(&line);
85+
break;
86+
}
87+
strbuf_trim(&line);
88+
rawargs = strbuf_detach(&line, NULL);
89+
split_args = xstrdup(rawargs);
90+
count = split_cmdline(split_args, &argv);
91+
if (count < 0) {
92+
fprintf(stderr, "invalid command format '%s': %s\n", rawargs,
93+
split_cmdline_strerror(count));
94+
free(split_args);
95+
free(rawargs);
96+
continue;
97+
}
98+
99+
prog = argv[0];
100+
if (!strcmp(prog, "")) {
101+
} else if (!strcmp(prog, "quit") || !strcmp(prog, "logout") ||
102+
!strcmp(prog, "exit") || !strcmp(prog, "bye")) {
103+
done = 1;
104+
} else if (is_valid_cmd_name(prog)) {
105+
full_cmd = make_cmd(prog);
106+
argv[0] = full_cmd;
107+
code = run_command_v_opt(argv, RUN_SILENT_EXEC_FAILURE);
108+
if (code == -1 && errno == ENOENT) {
109+
fprintf(stderr, "unrecognized command '%s'\n", prog);
110+
}
111+
free(full_cmd);
112+
} else {
113+
fprintf(stderr, "invalid command format '%s'\n", prog);
114+
}
115+
116+
free(argv);
117+
free(rawargs);
118+
} while (!done);
119+
}
36120

37121
static struct commands {
38122
const char *name;
@@ -48,8 +132,10 @@ static struct commands {
48132
int main(int argc, char **argv)
49133
{
50134
char *prog;
135+
const char **user_argv;
51136
struct commands *cmd;
52137
int devnull_fd;
138+
int count;
53139

54140
/*
55141
* Always open file descriptors 0/1/2 to avoid clobbering files
@@ -66,17 +152,28 @@ int main(int argc, char **argv)
66152
/*
67153
* Special hack to pretend to be a CVS server
68154
*/
69-
if (argc == 2 && !strcmp(argv[1], "cvs server"))
155+
if (argc == 2 && !strcmp(argv[1], "cvs server")) {
70156
argv--;
157+
} else if (argc == 1) {
158+
/* Allow the user to run an interactive shell */
159+
cd_to_homedir();
160+
if (access(COMMAND_DIR, R_OK | X_OK) == -1) {
161+
die("Interactive git shell is not enabled.\n"
162+
"hint: ~/" COMMAND_DIR " should exist "
163+
"and have read and execute access.");
164+
}
165+
run_shell();
166+
exit(0);
167+
} else if (argc != 3 || strcmp(argv[1], "-c")) {
168+
/*
169+
* We do not accept any other modes except "-c" followed by
170+
* "cmd arg", where "cmd" is a very limited subset of git
171+
* commands or a command in the COMMAND_DIR
172+
*/
173+
die("Run with no arguments or with -c cmd");
174+
}
71175

72-
/*
73-
* We do not accept anything but "-c" followed by "cmd arg",
74-
* where "cmd" is a very limited subset of git commands.
75-
*/
76-
else if (argc != 3 || strcmp(argv[1], "-c"))
77-
die("What do you think I am? A shell?");
78-
79-
prog = argv[2];
176+
prog = xstrdup(argv[2]);
80177
if (!strncmp(prog, "git", 3) && isspace(prog[3]))
81178
/* Accept "git foo" as if the caller said "git-foo". */
82179
prog[3] = '-';
@@ -99,5 +196,21 @@ int main(int argc, char **argv)
99196
}
100197
exit(cmd->exec(cmd->name, arg));
101198
}
102-
die("unrecognized command '%s'", prog);
199+
200+
cd_to_homedir();
201+
count = split_cmdline(prog, &user_argv);
202+
if (count >= 0) {
203+
if (is_valid_cmd_name(user_argv[0])) {
204+
prog = make_cmd(user_argv[0]);
205+
user_argv[0] = prog;
206+
execv(user_argv[0], (char *const *) user_argv);
207+
}
208+
free(prog);
209+
free(user_argv);
210+
die("unrecognized command '%s'", argv[2]);
211+
} else {
212+
free(prog);
213+
die("invalid command format '%s': %s", argv[2],
214+
split_cmdline_strerror(count));
215+
}
103216
}

0 commit comments

Comments
 (0)