Files
php-git-server/lib/git_service.php
T
2026-08-14 17:44:34 +08:00

524 lines
16 KiB
PHP

<?php
function git_service_executable_available($application) {
if (!isset($application['git_executable'])
|| !is_string($application['git_executable'])
|| $application['git_executable'] === '') {
return FALSE;
}
$executable = $application['git_executable'];
if (strpos($executable, DIRECTORY_SEPARATOR) !== FALSE) {
return is_file($executable) && is_executable($executable);
}
$path = getenv('PATH');
if ($path === FALSE) {
return FALSE;
}
foreach (explode(PATH_SEPARATOR, $path) as $directory) {
$candidate = rtrim($directory, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$executable;
if (is_file($candidate) && is_executable($candidate)) {
return TRUE;
}
}
return FALSE;
}
function git_service_native_available() {
return function_exists('inflate_init')
&& function_exists('inflate_add')
&& function_exists('inflate_get_read_len')
&& function_exists('gzcompress')
&& function_exists('hash')
&& in_array('sha1', hash_algos(), TRUE);
}
function git_service_write_repository_file($path, $contents) {
return @file_put_contents($path, $contents, LOCK_EX) === strlen($contents);
}
/* A newly-created repository starts with an unborn main branch. If the first
push creates a differently named branch, make that branch the default. */
function git_service_update_unborn_head($git_path, $updated_refs) {
$head = resolve_ref($git_path, 'HEAD');
if ($head[1] !== NULL) {
return TRUE;
}
$head_path = get_safe_file_path($git_path, '/HEAD');
$contents = $head_path === FALSE ? FALSE : @file_get_contents($head_path);
if ($contents === FALSE
|| !preg_match('~^ref:\s*(refs/heads/[^\r\n]+)\s*$~', $contents)) {
return TRUE;
}
$branch = NULL;
foreach ($updated_refs as $ref) {
$resolved = resolve_ref($git_path, $ref);
if (strpos($ref, 'refs/heads/') === 0 && $resolved[1] !== NULL) {
$branch = $ref;
break;
}
}
if ($branch === NULL) {
return TRUE;
}
$new_contents = "ref: ".$branch."\n";
return @file_put_contents($git_path.'/HEAD', $new_contents, LOCK_EX)
=== strlen($new_contents);
}
function git_service_init_bare_repository_with_php($path) {
$directories = array(
'',
'/branches',
'/hooks',
'/info',
'/objects',
'/objects/info',
'/objects/pack',
'/refs',
'/refs/heads',
'/refs/tags');
foreach ($directories as $directory) {
if (!@mkdir($path.$directory, 0777) && !is_dir($path.$directory)) {
return FALSE;
}
}
$files = array(
'/HEAD' => "ref: refs/heads/main\n",
'/config' => "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = true\n",
'/description' => "Unnamed repository; edit this file 'description' to name the repository.\n");
foreach ($files as $name => $contents) {
if (!git_service_write_repository_file($path.$name, $contents)) {
return FALSE;
}
}
return TRUE;
}
function git_service_init_bare_repository($application, $path) {
if (!function_exists('proc_open') || !git_service_executable_available($application)) {
return git_service_init_bare_repository_with_php($path);
}
$error = @tmpfile();
if ($error === FALSE) {
return FALSE;
}
$descriptor_spec = array(
0 => array('file', '/dev/null', 'r'),
1 => array('file', '/dev/null', 'w'),
2 => $error);
$pipes = array();
$command = array(
$application['git_executable'],
'init',
'--bare',
'--quiet',
$path);
$request = array('git_protocol' => NULL, 'user' => get_authenticated_user());
$process = @proc_open(
$command,
$descriptor_spec,
$pipes,
dirname($path),
git_service_environment($request));
if (!is_resource($process)) {
fclose($error);
return FALSE;
}
$exit_code = proc_close($process);
if ($exit_code !== 0) {
rewind($error);
$message = stream_get_contents($error);
error_log(
'Git repository initialization failed for '.$path.' with exit code '
.$exit_code.': '.trim($message));
}
fclose($error);
if ($exit_code !== 0) {
return FALSE;
}
$head = "ref: refs/heads/main\n";
return @file_put_contents($path.'/HEAD', $head, LOCK_EX) === strlen($head);
}
function git_service_create_managed_repository(
$application,
$configuration,
$value,
$owner_user_id,
$private) {
$name = normalize_managed_repository_name($value);
if ($name === FALSE) {
return array('status' => 'invalid_name');
}
$root = managed_repository_root($configuration);
if ($root === FALSE || !is_writable($root) || @scandir($root) === FALSE) {
return array('status' => 'root_unavailable');
}
$path = $root.DIRECTORY_SEPARATOR.$name;
$lock_path = $root.DIRECTORY_SEPARATOR.'.create.lock';
$lock = @fopen($lock_path, 'c+b');
if ($lock === FALSE) {
$status = file_exists($path) || is_link($path) ? 'already_exists' : 'create_failed';
return array('status' => $status, 'name' => $name);
}
if (!@flock($lock, LOCK_EX | LOCK_NB)) {
fclose($lock);
return array('status' => 'create_busy', 'name' => $name);
}
$temporary_path = NULL;
try {
if (file_exists($path) || is_link($path)) {
if (managed_repository_is_bare($path)) {
$recovery = auth_recover_repository_metadata(
$name, (int) $owner_user_id, $private);
if ($recovery['status'] === 'recovered') {
return array(
'status' => 'created',
'name' => $name,
'path' => $path,
'private' => (bool) $private);
}
if ($recovery['status'] === 'database_unavailable') {
return array('status' => 'metadata_unavailable', 'name' => $name);
}
}
return array('status' => 'already_exists', 'name' => $name);
}
try {
$suffix = bin2hex(random_bytes(16));
} catch (Exception $exception) {
return array('status' => 'create_failed', 'name' => $name);
}
$temporary_path = $root.DIRECTORY_SEPARATOR.'.create-'.$suffix.'.tmp';
if (!git_service_init_bare_repository($application, $temporary_path)
|| !managed_repository_is_bare($temporary_path)) {
return array('status' => 'create_failed', 'name' => $name);
}
if (file_exists($path) || is_link($path)) {
return array('status' => 'already_exists', 'name' => $name);
}
$reservation = auth_reserve_repository_metadata(
$name, (int) $owner_user_id, $private);
if ($reservation['status'] === 'already_exists') {
return array('status' => 'already_exists', 'name' => $name);
}
if ($reservation['status'] !== 'reserved') {
return array('status' => 'metadata_unavailable', 'name' => $name);
}
if (!@rename($temporary_path, $path)) {
return array('status' => 'create_failed', 'name' => $name);
}
$temporary_path = NULL;
if (!auth_complete_repository_metadata(
$reservation['id'], (int) $owner_user_id)) {
return array('status' => 'metadata_unavailable', 'name' => $name);
}
return array(
'status' => 'created',
'name' => $name,
'path' => $path,
'private' => (bool) $private);
} finally {
if ($temporary_path !== NULL) {
remove_managed_repository_directory($temporary_path);
}
flock($lock, LOCK_UN);
fclose($lock);
}
}
function git_service_is_protocol_v2($request) {
if ($request['git_protocol'] === NULL) {
return FALSE;
}
return preg_match('~(?:^|:)version=2(?:$|:)~', $request['git_protocol']) === 1;
}
function git_service_environment($request) {
$environment = getenv();
if (!is_array($environment)) {
$environment = array();
}
unset($environment['GIT_DIR']);
unset($environment['GIT_WORK_TREE']);
unset($environment['GIT_PROTOCOL']);
if ($request['git_protocol'] !== NULL
&& strlen($request['git_protocol']) <= 1024
&& strpos($request['git_protocol'], "\0") === FALSE
&& strpos($request['git_protocol'], "\n") === FALSE
&& strpos($request['git_protocol'], "\r") === FALSE) {
$environment['GIT_PROTOCOL'] = $request['git_protocol'];
}
if ($request['user'] !== NULL) {
$environment['REMOTE_USER'] = $request['user'];
}
if (isset($_SERVER['REMOTE_ADDR'])) {
$environment['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'];
}
if (isset($_SERVER['HTTP_USER_AGENT'])) {
$environment['GIT_HTTP_USER_AGENT'] = $_SERVER['HTTP_USER_AGENT'];
}
return $environment;
}
function git_service_command($application, $service, $repository, $advertise) {
if ($service === 'git-upload-pack') {
$subcommand = 'upload-pack';
} else if ($service === 'git-receive-pack') {
$subcommand = 'receive-pack';
} else {
return FALSE;
}
$command = array(
$application['git_executable'],
$subcommand,
'--stateless-rpc');
if ($subcommand === 'upload-pack') {
$command[] = '--strict';
}
if ($advertise) {
$command[] = '--advertise-refs';
}
$command[] = $repository['path'];
return $command;
}
function git_service_pump_process($pipes, $input) {
stream_set_blocking($pipes[0], FALSE);
stream_set_blocking($pipes[1], FALSE);
$input_done = $input === NULL;
$input_buffer = '';
$stdin_open = TRUE;
$stdout_open = TRUE;
while ($stdin_open || $stdout_open) {
if (!$input_done && strlen($input_buffer) < 65536) {
$chunk = fread($input, 65536);
if ($chunk === FALSE) {
return FALSE;
}
if ($chunk !== '') {
$input_buffer .= $chunk;
}
if (feof($input)) {
$input_done = TRUE;
}
}
if ($stdin_open && $input_done && $input_buffer === '') {
fclose($pipes[0]);
$stdin_open = FALSE;
}
$read = $stdout_open ? array($pipes[1]) : array();
$write = $stdin_open && $input_buffer !== '' ? array($pipes[0]) : array();
$except = NULL;
if (empty($read) && empty($write)) {
continue;
}
$ready = @stream_select($read, $write, $except, 30);
if ($ready === FALSE) {
return FALSE;
}
if (!empty($write)) {
$written = fwrite($pipes[0], $input_buffer);
if ($written === FALSE) {
return FALSE;
}
if ($written > 0) {
$input_buffer = (string) substr($input_buffer, $written);
}
}
if (!empty($read)) {
$output = fread($pipes[1], 65536);
if ($output === FALSE) {
return FALSE;
}
if ($output !== '') {
echo $output;
}
if (feof($pipes[1])) {
fclose($pipes[1]);
$stdout_open = FALSE;
}
}
}
return TRUE;
}
function git_service_run(
$application,
$repository,
$request,
$service,
$advertise,
$input=NULL,
$prefix='') {
if (!function_exists('proc_open') || !git_service_executable_available($application)) {
send_error(503, 'Service Unavailable', 'Git Smart HTTP is not available.');
}
$command = git_service_command($application, $service, $repository, $advertise);
if ($command === FALSE) {
send_error(403, 'Forbidden', 'Unsupported Git service.');
}
$error = @tmpfile();
if ($error === FALSE) {
send_error(500, 'Internal Server Error', 'Unable to create a Git error stream.');
}
$descriptor_spec = array(
0 => array('pipe', 'r'),
1 => array('pipe', 'w'),
2 => $error);
$pipes = array();
$process = @proc_open(
$command,
$descriptor_spec,
$pipes,
dirname($repository['path']),
git_service_environment($request));
if (!is_resource($process)) {
fclose($error);
send_error(503, 'Service Unavailable', 'Unable to start the Git service.');
}
if ($prefix !== '') {
echo $prefix;
}
$stream_succeeded = git_service_pump_process($pipes, $input);
foreach ($pipes as $pipe) {
if (is_resource($pipe)) {
fclose($pipe);
}
}
$exit_code = proc_close($process);
if (!$stream_succeeded || $exit_code !== 0) {
rewind($error);
$message = stream_get_contents($error);
error_log(
'Git service '.$service.' failed for '.$repository['url'].' with exit code '
.$exit_code.': '.trim($message));
}
fclose($error);
return $stream_succeeded && $exit_code === 0 ? 0 : $exit_code;
}
function git_service_advertise($application, $repository, $request, $service) {
if ((!function_exists('proc_open') || !git_service_executable_available($application))
&& !git_service_native_available()) {
send_error(503, 'Service Unavailable', 'The native Git service requires PHP zlib and hash support.');
}
repository_header_nocache($repository);
header('Content-Type: application/x-'.$service.'-advertisement');
header('X-Content-Type-Options: nosniff');
if ((!function_exists('proc_open') || !git_service_executable_available($application))
&& $service === 'git-upload-pack') {
echo format_packet_line('# service='.$service."\n").'0000';
if (!git_upload_pack_advertise_native($repository)) {
error_log('Native Git advertisement failed for '.$repository['url'].'.');
}
return;
}
if ((!function_exists('proc_open') || !git_service_executable_available($application))
&& $service === 'git-receive-pack') {
echo format_packet_line('# service='.$service."\n").'0000';
if (!git_receive_pack_advertise_native($repository)) {
error_log('Native Git receive advertisement failed for '.$repository['url'].'.');
}
return;
}
$prefix = '';
if (!git_service_is_protocol_v2($request)) {
$prefix = format_packet_line('# service='.$service."\n").'0000';
}
git_service_run($application, $repository, $request, $service, TRUE, NULL, $prefix);
}
function git_service_rpc($application, $repository, $request, $service, $input) {
if ((!function_exists('proc_open') || !git_service_executable_available($application))
&& !git_service_native_available()) {
send_error(503, 'Service Unavailable', 'The native Git service requires PHP zlib and hash support.');
}
repository_header_nocache($repository);
header('Content-Type: application/x-'.$service.'-result');
header('X-Content-Type-Options: nosniff');
if ((!function_exists('proc_open') || !git_service_executable_available($application))
&& $service === 'git-upload-pack') {
if (!git_upload_pack_rpc_native($repository, $input)) {
error_log('Native Git upload-pack failed for '.$repository['url'].'.');
}
return;
}
if ((!function_exists('proc_open') || !git_service_executable_available($application))
&& $service === 'git-receive-pack') {
if (!git_receive_pack_rpc_native($repository, $input)) {
error_log('Native Git receive-pack failed for '.$repository['url'].'.');
return 1;
}
return 0;
}
return git_service_run($application, $repository, $request, $service, FALSE, $input);
}