#!/usr/bin/perl

# ----------------------------------------------------------------------------
# ssh mode
# - started by sshd
# - one optional flag, "-s", for "shell allowed" people
# - one argument, the "user" name
# - one env var, SSH_ORIGINAL_COMMAND, containing the command
# - command typically: git-(receive|upload)-pack 'reponame(.git)?'
# - special gitolite commands: info, expand, (get|set)(perms|desc)
# - other commands: anything in $GL_ADC_PATH if defined (see rc file)
#
# (smart) http mode
# - started by apache (httpd)
# - no arguments
# - REQUEST_URI contains verb and repo, REMOTE_USER contains username
# - REQUEST_URI looks like /path/reponame.git/(info/refs\?service=)?git-(receive|upload)-pack
# - no special processing commands currently handled
# ----------------------------------------------------------------------------

use strict;
use warnings;

# ----------------------------------------------------------------------------
#       find the rc file, then pull the libraries in
# ----------------------------------------------------------------------------

# this (gl-auth-command) is one of the two valid starting points for all of
# gitolite for normal operations (the other being gl-time).  All other
# programs are invoked either from this, or from something else (typically
# git-*-pack) in between).  They thus get the benefit of the environment
# variables that this code sets up.
BEGIN {
    # find and set bin dir
    $0 =~ m|^(/)?(.*)/| and $ENV{GL_BINDIR} = ($1 || "$ENV{PWD}/") . $2;
}

# our libraries are either in the same place the scripts are, or, as with
# RPM/DEB install, in some 'system' location that is already in perl's @INC
# anyway
use lib $ENV{GL_BINDIR};

use gitolite_rc;    # this does a "do" of the rc file
use gitolite_env;
use gitolite;

# ----------------------------------------------------------------------------
#       start...
# ----------------------------------------------------------------------------

# these two options are mutually exclusive.  And this program is not supposed
# to be called manually anyway
my $shell_allowed = (@ARGV and $ARGV[0] eq '-s' and shift);
my $program       = (@ARGV and $ARGV[0] eq '-e' and shift);

# setup the environment for the kids so they don't need to embark on the
# voyage of self-discovery above ;-)  [environment also means things like
# nice, umask, etc., not just the environment *variables*]
setup_environment();

# if one of the other programs is being invoked (see doc/hacking.mkd), exec it
exec(@ARGV) if $program;

# ----------------------------------------------------------------------------
#       set up GL_USER and (if reqd) SSH_ORIGINAL_COMMAND and SSH_CONNECTION
# ----------------------------------------------------------------------------

my $user;
if ($ENV{REQUEST_URI}) {
    die "fallback to DAV not supported\n" 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).
    simulate_ssh_connection();

    $ENV{REMOTE_USER} ||= $GL_HTTP_ANON_USER;   # see doc/http-backend.mkd
    $user = $ENV{GL_USER} = $ENV{REMOTE_USER};
} else {
    # no (more) arguments given in ssh mode?  default user is $USER
    # (fedorahosted works like this, and it is harmless for others)
    @ARGV = ($ENV{USER}) unless @ARGV;
    $user = $ENV{GL_USER} = shift;
}

# ----------------------------------------------------------------------------
#       SSH_ORIGINAL_COMMAND
# ----------------------------------------------------------------------------

# no SSH_ORIGINAL_COMMAND given: shell out or default to 'info'
unless ($ENV{SSH_ORIGINAL_COMMAND}) {
    shell_out() if $shell_allowed;  # doesn't return ('exec's out)
    $ENV{SSH_ORIGINAL_COMMAND} = 'info';
}

# quick sanity check for newlines; could be used to create fake log entries.
# Not an access violation but possibly an audit/compliance reporting violation
die "I don't like newlines in the command: $ENV{SSH_ORIGINAL_COMMAND}\n" if $ENV{SSH_ORIGINAL_COMMAND} =~ /[\n\r]/;

# admin defined commands; please see doc/admin-defined-commands.mkd
if ($GL_ADC_PATH and -d $GL_ADC_PATH) {
    try_adc();  # if it succeeds, this also 'exec's out
}

# get/set perms/desc for wild repos; also the 'expand' command
my $CUSTOM_COMMANDS=qr/^\s*(expand|(get|set)(perms|desc))\b/;
# note that all the subs called here chdir somewhere else and do not come
# back; they all blithely take advantage of the fact that processing custom
# commands is sort of a dead end for normal (git) processing
if ($ENV{SSH_ORIGINAL_COMMAND} =~ $CUSTOM_COMMANDS) {
    die "wildrepos disabled, sorry\n" unless $GL_WILDREPOS;
    run_custom_command($user);
    exit 0;
}

# non-git commands: if the command does NOT fit the pattern of a normal git
# command, send it off somewhere else...

# side notes on detecting a normal git command: the pattern we check allows
# old style as well as new style ("git-subcommand arg" or "git subcommand
# arg").  Currently, this is how git sends across the command (including the
# single quotes):
#       git-receive-pack 'reponame.git'

my ($verb, $repo) = ($ENV{SSH_ORIGINAL_COMMAND} =~ /^\s*(git\s+\S+|\S+)\s+'\/?(.*?)(?:\.git)?'/);
unless ( $verb and ( $verb eq 'git-init' or $verb =~ $R_COMMANDS or $verb =~ $W_COMMANDS ) and $repo and $repo =~ $REPONAME_PATT ) {
    special_cmd ($shell_allowed);
    exit 0;
}

# some final sanity checks
die "$repo ends with a slash; I don't like that\n" if $repo =~ /\/$/;
die "$repo has two consecutive periods; I don't like that\n" if $repo =~ /\.\./;

# save the reponame; too many things need this
$ENV{GL_REPO}=$repo;

# ----------------------------------------------------------------------------
#       the real git commands (git-receive-pack, etc...)
# ----------------------------------------------------------------------------

# we know the user and repo; we just need to know what perm he's trying for
# (aa == attempted access; setting this makes some later logic simpler)
my $aa = ($verb =~ $R_COMMANDS ? 'R' : 'W');

# writes may get redirected under certain conditions
if ( $aa eq 'W' and mirror_mode($repo) =~ /^slave of (\S+)/ ) {
    my $master = $1;
    die "$ABRT GL_HOSTNAME not set; rejecting push to non-local repo\n" unless $GL_HOSTNAME;
    die "$ABRT $GL_HOSTNAME not the master, please push to $master\n" unless mirror_redirectOK($repo, $GL_HOSTNAME);
    print STDERR "$GL_HOSTNAME ==== $user ($repo) ===> $master\n";
    exec("ssh", $master, "USER=$user", "SOC=$ENV{SSH_ORIGINAL_COMMAND}");
}

# first level permissions check

my ($perm, $creator, $wild);
if ( $GL_ALL_READ_ALL and $verb =~ $R_COMMANDS and -d "$REPO_BASE/$repo.git") {
    $perm = 'R';
} else {
    ($perm, $creator, $wild) = repo_rights($repo);
}
# it was missing, and you have create perms, so create it
new_wild_repo($repo, $user) if ($perm =~ /C/);

die "$aa access for $repo DENIED to $user
(Or there may be no repository at the given path. Did you spell it correctly?)\n" unless $perm =~ /$aa/;

# check if repo is write-enabled
check_repo_write_enabled($repo) if $aa eq 'W';

# run the pre-git hook if present (do this last, just before actually handing
# off to git).  Force its output to go to STDERR so the git client does not
# get confused, in case the code in the pre-git hook forgot.  To make it
# simple for the script, send in $aa (which will be 'R' or 'W') so now they
# have all three: GL_USER and GL_REPO in the env, and $aa as arg-1.
if (-x "$REPO_BASE/$repo.git/hooks/gl-pre-git") {
    system("cd $REPO_BASE/$repo.git; hooks/gl-pre-git $aa >&2");
    die "gl-pre-git hook failed ($?)\n" if $?;
}

# ----------------------------------------------------------------------------
#       over to git now
# ----------------------------------------------------------------------------

if ($ENV{REQUEST_URI}) {
    log_it($ENV{REQUEST_URI});
    exec $ENV{GIT_HTTP_BACKEND};
    # the GIT_HTTP_BACKEND env var should be set either by the rc file, or as
    # a SetEnv in the apache config somewhere
}

log_it();

$repo = "'$REPO_BASE/$repo.git'";
exec("git", "shell", "-c", "$verb $repo") unless $verb eq 'git-init';
