<?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 git_output($str) { echo sprintf("%04x", 4 + 4 + strlen($str)) . 'ERR ' . $str; } 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; git_output($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; 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); $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'); // Only normal keys can execute some commands if (strstr($flags, 'N')) $must_exit = rg_ssh_dispatch($db, $ip, $login_uid, $cmd_repo); else $must_exit = FALSE; // Only normal keys can update stats if (strstr($flags, 'N')) { // 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; $flags = ''; $conn_ui = array('uid' => 0, 'username' => 'anonymous user', 'organization' => 0); $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]."); if (strstr($flags, 'W')) { // We are a worker, the command may be only for clonning! if (strcmp($cmd, 'git-upload-pack') != 0) fatal('A worker can only clone!'); } // 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."); // 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_ORGANIZATION=" . $conn_ui['organization']); putenv("ROCKETGIT_LOGIN_UID=" . $login_uid); putenv("ROCKETGIT_LOGIN_USERNAME=" . $conn_ui['username']); putenv("ROCKETGIT_LOGIN_URL=" . rg_re_userpage($conn_ui)); putenv("ROCKETGIT_KEY_ID=" . $key_id); putenv("ROCKETGIT_REPO_ID=" . $ri['repo_id']); putenv("ROCKETGIT_REPO_PATH=" . $repo_path); putenv("ROCKETGIT_REPO_NAME=" . $ri['name']); putenv("ROCKETGIT_REPO_UID=" . $ri['uid']); putenv("ROCKETGIT_REPO_CLONE_URL=" . $ri['clone_url']); 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'; $r = rg_copy_tree($repo_path . '/refs/heads', $dst . '/heads/', 0755); if ($r !== TRUE) fatal("Internal error (cannot copy heads)"); $r = rg_copy_tree($repo_path . '/refs/tags', $dst . '/tags/', 0755); if ($r !== TRUE) fatal("Internal error (cannot copy tags)"); } $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(); ?>