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 (6003ee97940c38eca683adbf23525e279829fc81) (8,662B) (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 . "/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 . "/ver.php");

rg_prof_start("remote.php");

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

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 {
		echo "\n" . $str2;
	}
	exit(1);
}

umask(0022);

rg_log("Start ($rocketgit_version)...");
rg_log("_SERVER: " . rg_array2string($_SERVER));
// DEBUG SELinux
$label = @file_get_contents("/proc/self/attr/current");
rg_log("SELINUX: " . $label);

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

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

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

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

	// 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;
	if ($login_uid == 0)
		fatal("uid not provided!");
	rg_log("uid is $login_uid.");

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

	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);
	$ip = rg_fix_ip($_t[0]);

	info('== Welcome to RocketGit! ==');
	info('you are connecting from IP ' . $ip . '.');

	$conn_ui = rg_user_info($db, $login_uid, '', '');
	if ($conn_ui['exists'] != 1)
		fatal("User does not exists (conn).");
	info('you are connecting as user \'' . $conn_ui['username'] . '\'.');

	putenv('ROCKETGIT_INFO_SHOW=1');

	$must_exit = rg_ssh_dispatch($db, $ip, $login_uid, $cmd_repo);

	// 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, $conn_ui['uid'], $key_id, $ip, $cmd_repo);
	if ($_r !== TRUE)
		rg_internal_error("Cannot update key last_use!");

	if ($must_exit) {
		rg_prof_end("remote.php");
		rg_prof_log();
		exit(0);
	}
} else {
	rg_log("git-daemon connection...");

	// we have no client info
	$login_uid = 0;
	$key_id = 0;
	$conn_ui = array('uid' => 0, 'username' => 'anonymous user');

	$line = @fread(STDIN, 8000);
	$line_len = strlen($line);
	rg_log("line=[$line]");
	if ($line_len < 4)
		fatal("Line is too short!");
	$len = @hexdec(substr($line, 0, 4));
	if ($line_len < $len)
		fatal("Too less data ($line_len/$len) received!");

	// parse something like: 002bgit-upload-pack /aa.git[0x00]host=localhost:9418[0x00]
	$line = substr($line, 4);
	$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];

	$ip = rg_fix_ip(getenv("REMOTE_HOST"));
}

// Extracts command and computes permissions
if (strncasecmp($cmd_repo, "git-upload-pack", 15) == 0) {
	$cmd = "git-upload-pack";
	$needed_rights = "F";
	$push = 0;
} else if (strncasecmp($cmd_repo, "git-receive-pack", 16) == 0) {
	$cmd = "git-receive-pack";
	// We need push or anonymous push
	$needed_rights = "P|H";
	$push = 1;
} else {
	rg_log("Unknown command [$cmd_repo]");
	fatal("Unknown command!");
}

// extract repository name
$_t = substr($cmd_repo, strlen($cmd));
$_t = trim($_t, "' ");
$_t = ltrim($_t, "/");
$_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("host=[$host] cmd=[$cmd] prefix=[$prefix] user=[$user] repo=[$repo].");

// validity/security checks
// Load info about the owner
if (rg_user_ok($user) !== TRUE)
	fatal("User [$user] is invalid (" . rg_user_error() . ")!");
$owner_ui = rg_user_info($db, 0, $user, "");
if ($owner_ui['ok'] != 1)
	fatal("Internal problems. Try again later, please.");
if ($owner_ui['exists'] != 1)
	fatal("User does not exists (repo).");

// Loading info about the repository
if (rg_repo_ok($repo) !== TRUE)
	fatal("Repo is invalid (" . rg_repo_error() . ")");
$ri = rg_repo_info($db, 0, $owner_ui['uid'], $repo);
if ($ri['ok'] != 1)
	fatal("Internal problems. Try again later, please.");
if ($ri['exists'] != 1)
	fatal("Repo does not exists.");
if ($ri['deleted'] == 1)
	fatal("Repo has been deleted!");

$ls = rg_repo_lock_status($db, $ri['repo_id']);
if ($ls['ok'] != 1)
	fatal('Could not get lock status: ' . rg_repo_error());
if (($ls['status'] == 1) && ($conn_ui['uid'] != $ls['uid'])) {
	$_u = rg_user_info($db, $ls['uid'], '', '');
	if ($_u['exists'] == 1)
		$_user = $_u['username'];
	else
		$_user = '?';
	fatal('Repository has been locked user ' . $_user
		. ' at ' . gmdate('Y-m-d H:i', $ls['itime']) . ' UTC.'
		. ' Reason: ' . $ls['reason']);
}

$repo_path = rg_repo_path_by_id($owner_ui['uid'], $ri['repo_id']);
rg_log("repo_path=$repo_path.");

// TODO: signal user that the repo moved and provide a hint how to follow

$x = array();
$x['obj_id'] = $ri['repo_id'];
$x['type'] = 'repo_refs';
$x['owner'] = $ri['uid'];
$x['uid'] = $conn_ui['uid'];
$x['username'] = $conn_ui['username'];
$x['needed_rights'] = $needed_rights;
$x['ip'] = $ip;
$x['misc'] = '';
$ret = rg_rights_allow($db, $x);
if ($ret !== TRUE)
	fatal("You have no rights to access this repo!");

// If we are enrolled, ask for login token
// For push we always ask for it, for fetch only if repo is NOT public
// And we can ask only if we have a ssh connection. For git protocol we cannot
// because we do not have the username/uid of the connecting user.
if (isset($_SERVER['SSH_CONNECTION'])) {
	if (($ri['public'] == 0) || ($push == 1)) {
		$r = rg_totp_verify_ip($db, $conn_ui['uid'], $ip);
		if (($r['ok'] == 0) && (empty($r['list'])))
			fatal(rg_totp_error() . '.');
	}
}

// TODO: limit per connection
// TODO: limit time and/or cpu
// TODO: limit cpuset
// TODO: limit io
// TODO: put process in a cgroup?


if (($push == 1) && rg_user_over_limit($db, $owner_ui, $max))
	fatal("Cannot push: user is over limit"
		. " (" . $owner_ui['disk_used_mb']. "MiB >= " . $max . "MiB)");

// Put in environment all we need
putenv("ROCKETGIT_LOGIN_UID=" . $login_uid);
putenv("ROCKETGIT_LOGIN_USERNAME=" . $conn_ui['username']);
putenv("ROCKETGIT_KEY_ID=" . $key_id);
putenv("ROCKETGIT_REPO_ID=" . $ri['repo_id']);
putenv("ROCKETGIT_REPO_PATH=" . $repo_path);
putenv("ROCKETGIT_REPO_UID=" . $ri['uid']);
putenv("ROCKETGIT_IP=$ip");
putenv("ROCKETGIT_ITIME=" . microtime(TRUE));
putenv("ROCKETGIT_HOST=" . $host);
if ($push == 1) {
	$namespace = "rg_" . rg_id(8);
	rg_log("namespace is $namespace.");
	putenv("GIT_NAMESPACE=" . $namespace);

	// Prepare refs to avoid the following message:
	// "No refs in common and none specified; doing nothing.
	// Perhaps you should specify a branch such as 'master'."
	$dst = $repo_path . "/refs/namespaces/" . $namespace . "/refs/heads";
	$r = rg_copy_tree($repo_path . "/refs/heads", $dst . "/", 0755);
	if ($r !== TRUE)
		fatal("Internal error (cannot copy refs)");
}

$run = "git-shell -c \"" . $cmd . " " . escapeshellarg($repo_path) . "\"";
$run = $cmd . ' ' . escapeshellarg($repo_path);
rg_log("Running [$run]...");
rg_prof_start($cmd);
passthru($run, $ret);
rg_prof_end($cmd);
rg_log("[$run] returned $ret.");

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