[a-zA-Z0-9][a-zA-Z0-9@._-]*(/[a-zA-Z0-9][a-zA-Z0-9@._-]*)*)\'$#'; public $commands_readonly = array('git-upload-pack', 'git upload-pack'); public $commands_write = array('git-receive-pack', 'git receive-pack'); /** * Serve a git request. * * @param string Username. * @param string Command to be run. */ public function serve($username, $cmd) { if (false !== strpos($cmd, "\n")) { throw new Exception('Command may not contain newline.'); } $splitted = preg_split('/\s/', $cmd, 2); if (count($splitted) != 2) { throw new Exception('Unknown command denied.'); } if ($splitted[0] == 'git') { $sub_splitted = preg_split('/\s/', $splitted[1], 2); if (count($sub_splitted) != 2) { throw new Exception('Unknown command denied.'); } $verb = sprintf('%s %s', $splitted[0], $sub_splitted[0]); $args = $sub_splitted[1]; } else { $verb = $splitted[0]; $args = $splitted[1]; } if (!in_array($verb, $this->commands_write) and !in_array($verb, $this->commands_readonly)) { throw new Exception('Unknown command denied.'); } if (!preg_match($this->preg, $args, $matches)) { throw new Exception('Arguments to command look dangerous.'); } $path = $matches['path']; // Check read/write rights $new_path = $this->haveAccess($username, $path, 'writable'); if ($new_path == false) { $new_path = $this->haveAccess($username, $path, 'readonly'); if ($new_path == false) { throw new Exception('Repository read access denied.'); } if (in_array($verb, $this->commands_write)) { throw new Exception('Repository write access denied.'); } } list($topdir, $relpath) = $new_path; $repopath = sprintf('%s.git', $relpath); $fullpath = $topdir.DIRECTORY_SEPARATOR.$repopath; if (!file_exists($fullpath) and in_array($verb, $this->commands_write)) { // it doesn't exist on the filesystem, but the // configuration refers to it, we're serving a write // request, and the user is authorized to do that: create // the repository on the fly $p = explode(DIRECTORY_SEPARATOR, $fullpath); $mpath = implode(DIRECTORY_SEPARATOR, array_slice($p, 0, -1)); mkdir($mpath, 0750, true); $this->initRepository($fullpath); $this->setGitExport($relpath, $fullpath); } $new_cmd = sprintf("%s '%s'", $verb, $fullpath); return $new_cmd; } /** * Main function called by the serve script. */ public static function main($argv, $env) { if (count($argv) != 1) { print('Missing argument USER.'); exit(1); } $username = $argv[0]; umask(0022); if (!isset($env['SSH_ORIGINAL_COMMAND'])) { print('Need SSH_ORIGINAL_COMMAND in environment.'); exit(1); } $cmd = $env['SSH_ORIGINAL_COMMAND']; chdir(Pluf::f('idf_plugin_syncgit_git_home_dir', '/home/git')); $serve = new IDF_Plugin_SyncGit_Serve(); try { $new_cmd = $serve->serve($username, $cmd); } catch (Exception $e) { print($e->getMessage()); exit(1); } passthru(sprintf('git shell -c %s', $new_cmd), $res); if ($res != 0) { print('Cannot execute git-shell.'); exit(1); } exit(); } /** * Control the access rights to the repository. * * @param string Username * @param string Path including the possible .git * @param string Type of access. 'readonly' or ('writable') * @return mixed False or array(base_git_reps, relative path to repo) */ public function haveAccess($username, $path, $mode='writable') { if ('.git' == substr($path, -4)) { $path = substr($path, 0, -4); } $sql = new Pluf_SQL('shortname=%s', array($path)); $projects = Pluf::factory('IDF_Project')->getList(array('filter'=>$sql->gen())); if ($projects->count() != 1) { return false; } $project = $projects[0]; $conf = new IDF_Conf(); $conf->setProject($project); $scm = $conf->getVal('scm', 'git'); if ($scm != 'git') { return false; } $sql = new Pluf_SQL('login=%s', array($username)); $users = Pluf::factory('Pluf_User')->getList(array('filter'=>$sql->gen())); if ($users->count() != 1 or !$users[0]->active) { return false; } $user = $users[0]; $request = new StdClass(); $request->user = $user; if (true === IDF_Precondition::accessTabGeneric($request, 'source_access_rights')) { if ($mode == 'readonly') { return array(Pluf::f('idf_plugin_syncgit_base_repositories', '/home/git/repositories'), $project->shortname); } if (true === IDF_Precondition::projectMemberOrOwner($request)) { return array(Pluf::f('idf_plugin_syncgit_base_repositories', '/home/git/repositories'), $project->shortname); } } return false; } /** * Init a new empty bare repository. * * @param string Full path to the repository */ public function initRepository($fullpath) { mkdir($fullpath, 0750, true); exec(sprintf('git --git-dir=%s init', escapeshellarg($fullpath)), $out, $res); if ($res != 0) { throw new Exception(sprintf('Init repository error, exit status %d.', $res)); } } /** * Set the git export value. * * @param string Relative path of the repository (not .git) * @param string Full path of the repository with .git */ public function setGitExport($relpath, $fullpath) { $sql = new Pluf_SQL('shortname=%s', array($relpath)); $projects = Pluf::factory('IDF_Project')->getList(array('filter'=>$sql->gen())); if ($projects->count() != 1) { return $this->gitExportDeny($fullpath); } $project = $projects[0]; $conf = new IDF_Conf(); $conf->setProject($project); $scm = $conf->getVal('scm', 'git'); if ($scm != 'git' or $project->private) { return $this->gitExportDeny($fullpath); } if ('all' == $conf->getVal('source_access_rights', 'all')) { return $this->gitExportAllow($fullpath); } return $this->gitExportDeny($fullpath); } /** * Remove the export flag. * * @param string Full path to the repository */ public function gitExportDeny($fullpath) { @unlink($fullpath.DIRECTORY_SEPARATOR.'git-daemon-export-ok'); if (file_exists($fullpath.DIRECTORY_SEPARATOR.'git-daemon-export-ok')) { throw new Exception('Cannot remove git-daemon-export-ok file.'); } return true; } /** * Set the export flag. * * @param string Full path to the repository */ public function gitExportAllow($fullpath) { touch($fullpath.DIRECTORY_SEPARATOR.'git-daemon-export-ok'); if (!file_exists($fullpath.DIRECTORY_SEPARATOR.'git-daemon-export-ok')) { throw new Exception('Cannot create git-daemon-export-ok file.'); } return true; } }