xaizek / rocketgit (License: AGPLv3+) (since 2018-12-09)
Light and fast Git hosting solution suitable to serve both as a hub or as a personal code storage with its tickets, pull requests, API and much more.
<root> / scripts / remote.php (46d7156b3296709991440c2a3090efcfb081cc97) (8,264B) (mode 100644) [raw]
<?php
// It is called by a remote client that does a push/fetch by git/ssh.
error_reporting(E_ALL);
ini_set("track_errors", "On");

require_once("/etc/rocketgit/config.php");

$INC = dirname(__FILE__) . "/../inc";
require_once($INC . "/init.inc.php");
require_once($INC . "/util.inc.php");
require_once($INC . "/log.inc.php");
require_once($INC . "/sql.inc.php");
require_once($INC . "/struct.inc.php");
require_once($INC . "/user.inc.php");
require_once($INC . "/repo.inc.php");
require_once($INC . "/prof.inc.php");
require_once($INC . "/ssh.inc.php");
require_once($INC . "/keys.inc.php");
require_once($INC . "/fixes.inc.php");
require_once($INC . "/plan.inc.php");
require_once($INC . '/stats.inc.php');
require_once($INC . "/ver.php");

rg_prof_start("remote.php");

rg_log_set_file($rg_log_dir . "/remote.log");

$rg = array();
$rg['start'] = microtime(TRUE);

function info($str)
{
	rg_log("Sending: " . $str);
	$str2 = "RocketGit: Info: " . $str . "\n";
	if (isset($_SERVER['SSH_CONNECTION'])) {
		// ssh
		fwrite(STDERR, $str2);
	} else {
		// Keep in mind we did not negotiated yet the side-band feat.
		// So, seems we cannot send nice messages to the user.
	}
}

function fatal($str)
{
	rg_log("Sending: " . $str);
	$str2 = "RocketGit: Error: " . $str . "\n";
	if (isset($_SERVER['SSH_CONNECTION'])) {
		// ssh
		fwrite(STDERR, $str2);
	} else {
		// See above for 'info' function
		// But, keep in mind that at least git archive --remote
		// expects a 4 byte len!
		//echo "\n" . $str2;
		echo rg_git_pack('ERR ' . $str2);
	}
	exit(1);
}

@umask(0022);

// Without next lines the STDOUT/STDERR are mixed
@stream_set_write_buffer(STDOUT, 0);
@stream_set_write_buffer(STDERR, 0);


rg_log("Start ($rocketgit_version)...");
// DEBUG SELinux
$label = @file_get_contents("/proc/self/attr/current");
if (!empty($label))
	rg_log('SELINUX: ' . $label);

rg_sql_app('rg-remote-' . $rg_log_sid);
$db = rg_sql_open($rg_sql);
if ($db === FALSE)
	fatal("Internal error (db)!");

// Force ste state loading, for sure we will need it
rg_cache_get('state');

if (rg_struct_ok($db) === FALSE)
	fatal("We are in a short maintenance window. Try again later.");

$rg['hostname'] = rg_state_get($db, 'hostname');
$rg['http_allow'] = rg_state_get($db, 'http_allow');
$rg['https_allow'] = rg_state_get($db, 'https_allow');
rg_base_url_build($rg['hostname'], $rg['http_allow'], $rg['https_allow']);
$rg['base_url'] = rg_base_url();
rg_log('DEBUG: base_url=' . rg_base_url());
$rg['debug'] = rg_state_get($db, 'debug');

rg_stats_conns_set('start', $rg['start']);

$login_ui = array('uid' => 0,
	'username' => 'anonymous user',
	'organization' => 0);

if (isset($_SERVER['SSH_CONNECTION'])) {
	rg_log("SSH connection: " . $_SERVER['SSH_CONNECTION']);

	rg_stats_conns_set('type', 'ssh');

	// we do not have host info
	$host = '';

	// first parameter must be uid of the user
	$login_uid = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : 0;
	rg_log("uid is $login_uid.");

	// second parameter must be the ssh key id
	$key_id = isset($_SERVER['argv'][2]) ? $_SERVER['argv'][2] : 0;
	// TODO: because of build system, 0 may be valid.
	//if ($key_id == 0)
	//	fatal("key_id not provided!");
	rg_log("key_id is $key_id.");

	// Third para is 'flags'
	$flags = isset($_SERVER['argv'][3]) ? $_SERVER['argv'][3] : '';
	rg_log('flags=' . $flags);

	if (!isset($_SERVER['SSH_ORIGINAL_COMMAND']))
		$cmd_repo = "";
	else
		$cmd_repo = trim($_SERVER['SSH_ORIGINAL_COMMAND']);

	$ssh_client = getenv("SSH_CLIENT");
	$_t = explode(" ", $ssh_client);
	$rg['ip'] = rg_fix_ip($_t[0]);
	rg_stats_conns_set('ip', $rg['ip']);

	info('== Welcome to RocketGit! ==');
	info('you are connecting from IP ' . $rg['ip'] . ' by ssh.');
	info('date/time: ' . gmdate('Y-m-d H:i:s')
		. ', debug id ' . $rg_log_sid . '.');

	$must_exit = FALSE;
	if (strstr($flags, 'N')) {
		$login_ui = rg_user_info($db, $login_uid, '', '');
		if ($login_ui['exists'] != 1)
			fatal("User does not exists (conn).");
		info('you are connecting as user \''
			. $login_ui['username'] . '\'.' . "\n");

		putenv('ROCKETGIT_SHOW_INFO=0');

		// We assume that the login user is the target user and no repo
		rg_stats_conns_set('uid', $login_ui['uid']);
		rg_stats_conns_set('repo_id', 0);
		rg_stats_conns_set('type', 'ssh');

		// Only normal keys can execute some commands
		$must_exit = rg_ssh_dispatch($db, $rg['ip'], $login_ui, $cmd_repo);

		// Only normal keys can update stats
		// We do this operation after dispatch to not impact the latency.
		// TODO: This should be put in a queue for performance reasons
		// At the same time, update stats? events?
		$_r = rg_keys_update_use($db, $login_ui['uid'], $key_id, $rg['ip'],
			$cmd_repo);
		if ($_r !== TRUE)
			rg_internal_error("Cannot update key last_use!");

		if ($must_exit) {
			rg_stats_conns_insert($db);
			rg_prof_end('remote.php');
			rg_prof_log();
			exit(0);
		}
	}

	rg_stats_conns_set('type', 'git-over-ssh');
} else {
	rg_log("git-daemon connection...");
	rg_log_ml('_SERVER: ' . print_r($_SERVER, true));

	rg_stats_conns_set('type', 'git');

	// we have no client info
	$login_uid = 0;
	$key_id = 0;
	$flags = '';

	$line = ''; $line_len = 0; $len = 0;
	while (1) {
		$r = @fread(STDIN, 8000);
		if ($r === FALSE)
			fatal('Error in receive: ' . rg_php_err());

		if (empty($r))
			fatal("Too less data ($line_len/$len) received!");

		$line .= $r;
		$line_len += strlen($r);
		rg_log("line=[$line]");
		if ($line_len < 4)
			fatal('Line is too short [' . $line . ']!');

		$len = @hexdec(substr($line, 0, 4));
		rg_log('Decoded len=' . $len . ' line_len=' . $line_len);
		if ($line_len >= $len)
			break;
	}

	// parse something like: 002bgit-upload-pack /aa.git[0x00]host=localhost:9418[0x00]
	$line = substr($line, 4); // skip length
	$v = explode("\0", $line);
	$cmd_repo = trim($v[0]);
	$host_port = isset($v[1]) ? trim(substr($v[1], 5)) : '';
	$v = explode(':', $host_port);
	$host = $v[0];

	$rg['ip'] = rg_fix_ip(getenv("REMOTE_HOST"));
	rg_stats_conns_set('ip', $rg['ip']);
}

if (strncasecmp($cmd_repo, 'git-upload-pack', 15) == 0) {
	$rg['cmd'] = 'git-upload-pack';
} else if (strncasecmp($cmd_repo, 'git-receive-pack', 16) == 0) {
	$rg['cmd'] = 'git-receive-pack';
} else {
	$rg['cmd'] = $cmd_repo;
}
rg_stats_conns_set('cmd', $rg['cmd']);

// extract repository name
$rg['url'] = substr($cmd_repo, strlen($rg['cmd'])); // skip cmd
$rg['url'] = trim($rg['url'], "' ");
rg_stats_conns_set('url', $rg['url']);
$_t = ltrim($rg['url'], "/");
$_t = preg_replace('/\.git$/' , '', $_t);
$_t = explode("/", $_t);
if (strcmp($_t[0], "user") == 0) {
	$prefix = "/user";
	$user = isset($_t[1]) ? $_t[1] : "";
	$repo = isset($_t[2]) ? $_t[2] : "";
} else {
	$prefix = "";
	$user = isset($_t[0]) ? $_t[0] : "";
	$repo = isset($_t[1]) ? $_t[1] : "";
}

rg_log('ip=[' . $rg['ip'] . '] host=[' . $host . '] cmd=[' . $rg['cmd'] . ']'
	. ' prefix=[' . $prefix . '] user=[' . $user . ']'
	. ' repo=[' . $repo . '].');

if (strstr($flags, 'W')) {
	// We are a worker, the command may be only for cloning!
	if (strcmp($rg['cmd'], 'git-upload-pack') != 0)
		fatal('A worker can only clone!');
}

$helper_flags = '';
// bypass authentication because it is a global worker
if (($login_uid == 0) && strstr($flags, 'W'))
	$helper_flags .= 'B';

$r = rg_repo_fetch_push_helper($db, $host, $rg['ip'], $login_ui, $prefix,
	$user, $repo, $rg['cmd'], $helper_flags);
rg_log_ml('DEBUG: repo_fetch_push_helper: ' . print_r($r, TRUE));
if (($r['ok'] !== 1) || ($r['allow'] !== 1))
	fatal($r['errmsg']);

$run = "git-shell -c \"" . $rg['cmd'] . " " . escapeshellarg($r['repo_path']) . "\"";
//$run = $rg['cmd'] . ' ' . escapeshellarg($r['repo_path']);
rg_log("Running [$run]...");
rg_prof_start($rg['cmd']);
// TODO: shouldn't we use rg_exec to capture stderr?
passthru($run, $ret);
rg_prof_end($rg['cmd']);
rg_log("[$run] returned $ret.");

if (!strstr($flags, 'W')) {
	$repo_id = 0;
	$uid = 0;

	$_ui = rg_user_info($db, 0, $user, '');
	if ($_ui['exists'] == 1)
		$uid = $_ui['uid'];

	if ($uid > 0) {
		rg_stats_conns_set('uid', $uid);

		$_ri = rg_repo_info($db, 0, $uid, $repo);
		if ($_ri['exists'] == 1)
			$repo_id = $_ri['repo_id'];

		rg_stats_conns_set('repo_id', $repo_id);
	}

	rg_stats_conns_insert($db);
}

rg_prof_end('remote.php');
rg_prof_log();
Hints

Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://code.reversed.top/user/xaizek/rocketgit

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@code.reversed.top/user/xaizek/rocketgit

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a pull request:
... clone the repository ...
... make some changes and some commits ...
git push origin master