summaryrefslogtreecommitdiffstats
path: root/src/gitolite-shell
diff options
context:
space:
mode:
Diffstat (limited to '')
-rwxr-xr-xsrc/gitolite-shell262
1 files changed, 262 insertions, 0 deletions
diff --git a/src/gitolite-shell b/src/gitolite-shell
new file mode 100755
index 0000000..072e0ff
--- /dev/null
+++ b/src/gitolite-shell
@@ -0,0 +1,262 @@
+#!/usr/bin/perl
+
+# gitolite shell, invoked from ~/.ssh/authorized_keys
+# ----------------------------------------------------------------------
+
+use FindBin;
+
+BEGIN { $ENV{GL_BINDIR} = $FindBin::RealBin; }
+BEGIN { $ENV{GL_LIBDIR} = "$ENV{GL_BINDIR}/lib"; }
+use lib $ENV{GL_LIBDIR};
+
+# set HOME
+BEGIN { $ENV{HOME} = $ENV{GITOLITE_HTTP_HOME} if $ENV{GITOLITE_HTTP_HOME}; }
+
+use Gitolite::Rc;
+use Gitolite::Common;
+use Gitolite::Conf::Load;
+
+use strict;
+use warnings;
+
+# the main() sub expects ssh-ish things; set them up...
+my $id = '';
+if ( exists $ENV{G3T_USER} ) {
+ $id = in_file(); # file:// masquerading as ssh:// for easy testing
+} elsif ( exists $ENV{SSH_CONNECTION} ) {
+ $id = in_ssh();
+} elsif ( exists $ENV{REQUEST_URI} ) {
+ $id = in_http();
+} else {
+ _die "who the *heck* are you?";
+}
+
+# sanity...
+my $soc = $ENV{SSH_ORIGINAL_COMMAND};
+$soc =~ s/[\n\r]+/<<newline>>/g;
+_die "I don't like newlines in the command: '$soc'\n" if $ENV{SSH_ORIGINAL_COMMAND} ne $soc;
+
+# allow gitolite-shell to be used as "$SHELL". Experts only; no support, no docs
+if (@ARGV and $ARGV[0] eq '-c') {
+ shift;
+ $ARGV[0] =~ s/^$0 // or _die "unknown git/gitolite command: '$ARGV[0]'";
+}
+
+# the INPUT trigger massages @ARGV and $ENV{SSH_ORIGINAL_COMMAND} as needed
+trigger('INPUT');
+
+main($id);
+
+gl_log('END') if $$ == $ENV{GL_TID};
+
+exit 0;
+
+# ----------------------------------------------------------------------
+
+sub in_file {
+ gl_log( 'file', "ARGV=" . join( ",", @ARGV ), "SOC=$ENV{SSH_ORIGINAL_COMMAND}" );
+
+ if ( $ENV{SSH_ORIGINAL_COMMAND} =~ /git-\w+-pack/ ) {
+ print STDERR "TRACE: gsh(", join( ")(", @ARGV ), ")\n";
+ print STDERR "TRACE: gsh(SOC=$ENV{SSH_ORIGINAL_COMMAND})\n";
+ }
+ return 'file';
+}
+
+sub in_http {
+ http_setup_die_handler();
+
+ _die "GITOLITE_HTTP_HOME not set" unless $ENV{GITOLITE_HTTP_HOME};
+
+ _die "fallback to DAV not supported" if $ENV{REQUEST_METHOD} eq 'PROPFIND';
+
+ # fake out SSH_ORIGINAL_COMMAND and SSH_CONNECTION when called via http,
+ # so the rest of the code stays the same (except the exec at the end).
+ http_simulate_ssh_connection();
+ $ENV{SSH_ORIGINAL_COMMAND} ||= '';
+
+ $ENV{REMOTE_USER} ||= $rc{HTTP_ANON_USER};
+ @ARGV = ( $ENV{REMOTE_USER} );
+
+ my $ip;
+ ( $ip = $ENV{SSH_CONNECTION} || '(no-IP)' ) =~ s/ .*//;
+
+ gl_log( 'http', "ARGV=" . join( ",", @ARGV ), "SOC=" . ( $ENV{SSH_ORIGINAL_COMMAND} || '' ), "FROM=$ip" );
+
+ return 'http';
+}
+
+sub in_ssh {
+ my $ip;
+ ( $ip = $ENV{SSH_CONNECTION} || '(no-IP)' ) =~ s/ .*//;
+
+ gl_log( 'ssh', "ARGV=" . join( ",", @ARGV ), "SOC=" . ( $ENV{SSH_ORIGINAL_COMMAND} || '' ), "FROM=$ip" );
+
+ $ENV{SSH_ORIGINAL_COMMAND} ||= '';
+
+ return $ip;
+}
+
+# ----------------------------------------------------------------------
+
+# call this once you are sure arg-1 is the username and SSH_ORIGINAL_COMMAND
+# has been setup (even if it's not actually coming via ssh).
+sub main {
+ my $id = shift;
+
+ # set up the user
+ my $user = $ENV{GL_USER} = shift @ARGV;
+
+ # set up the repo and the attempted access
+ my ( $verb, $repo ) = parse_soc(); # returns only for git commands
+ Gitolite::Conf::Load::sanity($repo, $REPONAME_PATT);
+ $ENV{GL_REPO} = $repo;
+ my $aa = ( $verb =~ 'upload' ? 'R' : 'W' );
+
+ # set up env vars from options set for this repo
+ env_options($repo);
+
+ # auto-create?
+ if ( repo_missing($repo) and access( $repo, $user, '^C', 'any' ) !~ /DENIED/ ) {
+ require Gitolite::Conf::Store;
+ Gitolite::Conf::Store->import;
+ new_wild_repo( $repo, $user, $aa );
+ gl_log( 'create', $repo, $user, $aa );
+ }
+
+ # a ref of 'any' signifies that this is a pre-git check, where we don't
+ # yet know the ref that will be eventually pushed (and even that won't
+ # apply if it's a read operation). See the matching code in access() for
+ # more information.
+ unless ( $ENV{GL_BYPASS_ACCESS_CHECKS} ) {
+ my $ret = access( $repo, $user, $aa, 'any' );
+ trigger( 'ACCESS_1', $repo, $user, $aa, 'any', $ret );
+ _die $ret . "\n(or you mis-spelled the reponame)" if $ret =~ /DENIED/;
+
+ gl_log( "pre_git", $repo, $user, $aa, 'any', $ret );
+ }
+
+ trigger( 'PRE_GIT', $repo, $user, $aa, 'any', $verb );
+ if ( $ENV{REQUEST_URI} ) {
+ _system( "git", "http-backend" );
+ } else {
+ my $repodir = "'$rc{GL_REPO_BASE}/$repo.git'";
+ _system( "git", "shell", "-c", "$verb $repodir" );
+ }
+ trigger( 'POST_GIT', $repo, $user, $aa, 'any', $verb );
+}
+
+# ----------------------------------------------------------------------
+
+sub parse_soc {
+ my $soc = $ENV{SSH_ORIGINAL_COMMAND};
+ $soc ||= 'info';
+
+ my $git_commands = "git-upload-pack|git-receive-pack|git-upload-archive";
+ # simplify the regex; we'll handle all the reponame nuances later
+ if ( $soc =~ m(^($git_commands) '?/?(.*?)'?$) ) {
+ my ( $verb, $repo ) = ( $1, $2 );
+ trace( 2, "git command", $soc );
+
+ # clean up the repo name; first extract the trace level if supplied
+ # (and no, you can't have a trace level *and* a trailing slash).
+ $ENV{D} = $1 if $repo =~ s/\.git(\d)$//;
+ # and then the git-daemon-compatibility trailers
+ $repo =~ s(/$)();
+ $repo =~ s(\.git$)();
+
+ _die "invalid repo name: '$repo'" if $repo !~ $REPONAME_PATT;
+ return ( $verb, $repo );
+ }
+
+ # after this we should not return; caller expects us to handle it all here
+ # and exit out
+
+ my @words = split ' ', $soc;
+ if ( $rc{COMMANDS}{ $words[0] } ) {
+ if ( $rc{COMMANDS}{ $words[0] } ne 'ua' ) {
+ _die "suspicious characters loitering about '$soc'" if $soc !~ $REMOTE_COMMAND_PATT;
+ _die "no relative paths allowed anywhere!" if $soc =~ m(\.\./);
+ }
+ trace( 2, "gitolite command", $soc );
+ _system( "gitolite", @words );
+ exit 0;
+ }
+
+ _die "unknown git/gitolite command: '$soc'";
+}
+
+# ----------------------------------------------------------------------
+# helper functions for "in_http"
+
+sub http_setup_die_handler {
+
+ $SIG{__DIE__} = sub {
+ my $service = ( $ENV{SSH_ORIGINAL_COMMAND} =~ /git-receive-pack/ ? 'git-receive-pack' : 'git-upload-pack' );
+ my $message = shift; chomp($message);
+ print STDERR "$message\n";
+
+ http_print_headers($service);
+
+ # format the service response, then the message. With initial
+ # help from Ilari and then a more detailed email from Shawn...
+ $service = "# service=$service\n"; $message = "ERR $message\n";
+ $service = sprintf( "%04X", length($service) + 4 ) . "$service"; # no CRLF on this one
+ $message = sprintf( "%04X", length($message) + 4 ) . "$message";
+
+ print $service;
+ print "0000"; # flush-pkt, apparently
+ print $message;
+ print STDERR $service;
+ print STDERR $message;
+ exit 0; # if it's ok for die_webcgi in git.git/http-backend.c, it's ok for me ;-)
+ }
+}
+
+sub http_simulate_ssh_connection {
+ # these patterns indicate normal git usage; see "services[]" in
+ # http-backend.c for how I got that. Also note that "info" is overloaded;
+ # git uses "info/refs...", while gitolite uses "info" or "info?...". So
+ # there's a "/" after info in the list below
+ if ( $ENV{PATH_INFO} =~ m(^/(.*)/(HEAD$|info/refs$|objects/|git-(?:upload|receive)-pack$)) ) {
+ my $repo = $1;
+ my $verb = ( $ENV{REQUEST_URI} =~ /git-receive-pack/ ) ? 'git-receive-pack' : 'git-upload-pack';
+ $ENV{SSH_ORIGINAL_COMMAND} = "$verb '$repo'";
+ } else {
+ # this is one of our custom commands; could be anything really,
+ # because of the adc feature
+ my ($verb) = ( $ENV{PATH_INFO} =~ m(^/(\S+)) );
+ my $args = $ENV{QUERY_STRING};
+ $args =~ s/\+/ /g;
+ $args =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
+ $ENV{SSH_ORIGINAL_COMMAND} = $verb;
+ $ENV{SSH_ORIGINAL_COMMAND} .= " $args" if $args;
+ http_print_headers(); # in preparation for the eventual output!
+
+ # we also need to pipe STDERR out via STDOUT, else the user doesn't see those messages!
+ open(STDERR, ">&STDOUT") or _die "Can't dup STDOUT: $!";
+ }
+ $ENV{SSH_CONNECTION} = "$ENV{REMOTE_ADDR} $ENV{REMOTE_PORT} $ENV{SERVER_ADDR} $ENV{SERVER_PORT}";
+}
+
+my $http_headers_printed = 0;
+
+sub http_print_headers {
+ my ( $service, $code, $text ) = @_;
+
+ return if $http_headers_printed++;
+ $code ||= 200;
+ $text ||= "OK - gitolite";
+
+ $|++;
+ print "Status: $code $text\r\n";
+ print "Expires: Fri, 01 Jan 1980 00:00:00 GMT\r\n";
+ print "Pragma: no-cache\r\n";
+ print "Cache-Control: no-cache, max-age=0, must-revalidate\r\n";
+ if ($service) {
+ print "Content-Type: application/x-$service-advertisement\r\n";
+ } else {
+ print "Content-Type: text/plain\r\n";
+ }
+ print "\r\n";
+}