diff --git a/scripts/gitserve.php b/scripts/gitserve.php new file mode 100644 index 0000000..5c5b588 --- /dev/null +++ b/scripts/gitserve.php @@ -0,0 +1,41 @@ +[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'); + + /** + * Check that the command is authorized. + */ + + + /** + * 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('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('git_base_repositories', '/home/git/repositories'), + $project->shortname); + } + if (true === IDF_Precondition::projectMemberOrOwner($request)) { + return array(Pluf::f('git_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; + } +} diff --git a/src/IDF/Tests/TestSyncGit.php b/src/IDF/Tests/TestSyncGit.php new file mode 100644 index 0000000..3e120d4 --- /dev/null +++ b/src/IDF/Tests/TestSyncGit.php @@ -0,0 +1,44 @@ +preg; + $no_matches = array('foo', "'ev!l'", "'something/../evil'"); + foreach ($no_matches as $test) { + preg_match($regex, $test, $matches); + $this->assertEqual(false, isset($matches['path'])); + } + } +} \ No newline at end of file