#!/usr/bin/env perl

=pod

=head1 NAME

git-server - Secure Git Server with more granular hooks capabilities than default git.

=head1 SYNOPSIS

  Standard Method:
  With SHELL=/bin/bash, use the following format in ~/.ssh/authorized_keys:
  command="/path/to/git-server REMOTE_USER=user1" ssh-ed25519 AAAA_OAX+blah_pub__ user1@workstation

   -- OR --

  Advanced Method:
  Set SHELL=/path/to/git-server (in /etc/passwd) and
  Add /path/to/git-server to /etc/shells and
  Set "PermitUserEnvironment yes" (in /etc/ssh/sshd_config)
  Then use the following format in ~/.ssh/authorized_keys:
  environment="REMOTE_USER=user1" ssh-ed25519 AAAA_OAX+blah_pub__ user1@workstation

=head1 ENV

You can set as many %ENV variables as you want
within the authorized_keys configuration.

=head2 REMOTE_USER

REMOTE_USER has a special meaning to define a word for the associated user.
You may use the same REMOTE_USER for those who have multiple PubKeys.
This REMOTE_USER will be used for ACL rules.

=head1 INSTALL

This can be used with any existing git repositories or as a drop-in replacement
for git-shell or you can create a fresh repo on the git host:

  git init --bare project

Then add hooks/run-git-hooks to override the default behavior:

  vi project/hooks/run-git-hooks
  chmod 755 project/hooks/run-git-hooks

If hooks/run-git-hooks exists from within the repository being targeted,
then this will run with the correct GIT_DIR
and any other ENV settings defined in authorized_keys.
If it doesn't exist, then it will look for a way to use
these git-server hooks with this project.

=head1 SEE ALSO

Similar functionality to the following:

  gitlab-shell, gitolite, git-shell

=head1 AUTHOR

Rob Brown <bbb@cpan.org>

=head1 COPYRIGHT AND LICENSE

Copyright 2015-2026 by Rob Brown <bbb@cpan.org>

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

=cut

use strict;
use warnings;
use Cwd qw(abs_path);
use FindBin qw($Bin);

our $VERSION = "0.038";

$SIG{PIPE} = sub { exit 1; };
my $cmd = $ENV{SSH_ORIGINAL_COMMAND} ||= # Standard Method from ~/.ssh/authorized_keys: command="git-server REMOTE_USER=user1" # i.e., "git-upload-pack 'project'"
    @ARGV == 2 && $ARGV[0] eq "-c" && shift && shift || ""; # Advanced Method from /etc/passwd SHELL

while (my $pair = shift) {
    if ($pair =~ /^(\w+)=(.*)$/) {
        $ENV{$1} = $2;
    }
    else {
        die "Invalid ENV setting [$pair]\n";
    }
}

$ENV{REMOTE_USER} ||= $ENV{KEY} || "";
$ENV{SSH_CONNECTION} ||= "";
($ENV{REMOTE_ADDR}, $ENV{REMOTE_PORT}, $ENV{SERVER_ADDR}, $ENV{SERVER_PORT}) = split / /, $ENV{SSH_CONNECTION};
if (!$ENV{REMOTE_PORT} || !$ENV{SERVER_ADDR} || !$ENV{SERVER_PORT}
    || $cmd =~ m{(^|/)(git-|)verify}
    || !$ENV{REMOTE_ADDR} || $ENV{REMOTE_ADDR} !~ /^([\da-f\.:]+)$/) {
    (my $v = $0) =~ s{-[^/]+$}{-verify};
    exec $v or die "Verification failed\n";
}
$ENV{REMOTE_ADDR} = $1; # Taint cleaner
$ENV{REMOTE_USER} ||= "MISCONFIGURED_REMOTE_USER";
#delete $ENV{$_} foreach (qw[KEY SSH_CLIENT SSH_CONNECTION]); # No cheating
my $who = "$ENV{REMOTE_USER}\@$ENV{REMOTE_ADDR}";
die localtime().": [$who] git-server: You don't have shell access!\n" unless $cmd;

my $dir = undef;
if ($cmd =~ /^(git-[\w\-]+) (.+)$/) {
    my $op = $1;
    my $repo = $2;
    if ($repo =~ /^'(.+)'$/) {
        $repo = $1;
        $repo =~ s/\'\\\'\'/\'/g;
    }
    $repo =~ s/\.git$//;
    $repo =~ s/\/+$//;
    my $home = $ENV{HOME} || (getpwuid $<)[7];
    foreach my $try ("$repo.git/.git", "$repo.git", "$repo/.git", $repo) {
        if (-d $try) {
            $dir = $try;
            $ENV{GIT_DIR} = abs_path $dir;
            last;
        }
        if ($try =~ s{^/+}{} and -d $try) {
            $dir = $try;
            $ENV{GIT_DIR} = abs_path $dir;
            last;
        }
        if ($try =~ s{^~/}{$home/} and -d $try) {
            $dir = $try;
            $ENV{GIT_DIR} = abs_path $dir;
            last;
        }
    }
    # Make sure this looks like a real life sane enough git repo
    die localtime().": [$who] git-server: You can't access '$repo' git repository\n" unless $dir && -f "$dir/config" && -d "$dir/refs" && -d "$dir/objects";
    my $escape_dir = $ENV{GIT_DIR};
    $escape_dir =~ s/\'/\'\\'\'/g;
    $escape_dir = "'$escape_dir'";
    $cmd = "$op $escape_dir";
}
else {
    die localtime().": [$who] git-server: Unable to run the command: $cmd\n";
}

# Hand off request to the best handler
my $handler =
    -x "$ENV{GIT_DIR}/hooks/run-git-hooks" ? "$ENV{GIT_DIR}/hooks/run-git-hooks" :
    -x "$Bin/hooks/run-git-hooks" ? "$Bin/hooks/run-git-hooks" : "git-shell";
exec $handler, "-c", $cmd;
