<?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;␊ |
}␊ |
}␊ |