| <?php␊ |
| /* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */␊ |
| /*␊ |
| # ***** BEGIN LICENSE BLOCK *****␊ |
| # This file is part of InDefero, an open source project management application.␊ |
| # Copyright (C) 2008 Céondo Ltd and contributors.␊ |
| #␊ |
| # InDefero is free software; you can redistribute it and/or modify␊ |
| # it under the terms of the GNU General Public License as published by␊ |
| # the Free Software Foundation; either version 2 of the License, or␊ |
| # (at your option) any later version.␊ |
| #␊ |
| # InDefero is distributed in the hope that it will be useful,␊ |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of␊ |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the␊ |
| # GNU General Public License for more details.␊ |
| #␊ |
| # You should have received a copy of the GNU General Public License␊ |
| # along with this program; if not, write to the Free Software␊ |
| # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA␊ |
| #␊ |
| # ***** END LICENSE BLOCK ***** */␊ |
| ␊ |
| /**␊ |
| * Main application to serve git repositories through a restricted SSH␊ |
| * access.␊ |
| */␊ |
| class IDF_Plugin_SyncGit_Serve␊ |
| {␊ |
| /**␊ |
| * Regular expression to match the path in the git command.␊ |
| */␊ |
| public $preg = '#^\'/*(?P<path>[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;␊ |
| }␊ |
| }␊ |