-
Notifications
You must be signed in to change notification settings - Fork 42
webhooks #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
webhooks #36
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Satisfy All | ||
Deny from all |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,249 +1,15 @@ | ||
<?php | ||
|
||
namespace GitlabComposer; | ||
require __DIR__ . '/../vendor/autoload.php'; | ||
|
||
use Gitlab\Client; | ||
use Gitlab\Exception\RuntimeException; | ||
|
||
$packages_file = __DIR__ . '/../cache/packages.json'; | ||
$static_file = __DIR__ . '/../confs/static-repos.json'; | ||
|
||
/** | ||
* Output a json file, sending max-age header, then dies | ||
*/ | ||
$outputFile = function ($file) { | ||
$mtime = filemtime($file); | ||
|
||
header('Content-Type: application/json'); | ||
header('Last-Modified: ' . gmdate('r', $mtime)); | ||
header('Cache-Control: max-age=0'); | ||
|
||
if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) && ($since = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) && $since >= $mtime) { | ||
header('HTTP/1.0 304 Not Modified'); | ||
} else { | ||
readfile($file); | ||
} | ||
die(); | ||
}; | ||
|
||
// See ../confs/samples/gitlab.ini | ||
$config_file = __DIR__ . '/../confs/gitlab.ini'; | ||
if (!file_exists($config_file)) { | ||
header('HTTP/1.0 500 Internal Server Error'); | ||
die('confs/gitlab.ini missing'); | ||
} | ||
$confs = parse_ini_file($config_file); | ||
|
||
$client = Client::create($confs['endpoint']); | ||
$client->authenticate($confs['api_key'], Client::AUTH_URL_TOKEN); | ||
|
||
$groups = $client->api('groups'); | ||
$projects = $client->api('projects'); | ||
$repos = $client->api('repositories'); | ||
|
||
$validMethods = array('ssh', 'http'); | ||
if (isset($confs['method']) && in_array($confs['method'], $validMethods)) { | ||
define('method', $confs['method']); | ||
} else { | ||
define('method', 'ssh'); | ||
} | ||
|
||
$allow_package_name_mismatches = !empty($confs['allow_package_name_mismatch']); | ||
|
||
/** | ||
* Retrieves some information about a project's composer.json | ||
* | ||
* @param array $project | ||
* @param string $ref commit id | ||
* @return array|false | ||
*/ | ||
$fetch_composer = function($project, $ref) use ($repos, $allow_package_name_mismatches) { | ||
try { | ||
$c = $repos->getFile($project['id'], 'composer.json', $ref); | ||
|
||
if(!isset($c['content'])) { | ||
return false; | ||
} | ||
|
||
$composer = json_decode(base64_decode($c['content']), true); | ||
|
||
if (empty($composer['name']) || (!$allow_package_name_mismatches && strcasecmp($composer['name'], $project['path_with_namespace']) !== 0)) { | ||
return false; // packages must have a name and must match | ||
} | ||
|
||
return $composer; | ||
} catch (RuntimeException $e) { | ||
return false; | ||
} | ||
}; | ||
|
||
/** | ||
* Retrieves some information about a project for a specific ref | ||
* | ||
* @param array $project | ||
* @param string $ref commit id | ||
* @return array [$version => ['name' => $name, 'version' => $version, 'source' => [...]]] | ||
*/ | ||
$fetch_ref = function($project, $ref) use ($fetch_composer) { | ||
|
||
static $ref_cache = []; | ||
|
||
$ref_key = md5(serialize($project) . serialize($ref)); | ||
|
||
if (!isset($ref_cache[$ref_key])) { | ||
if (preg_match('/^v?\d+\.\d+(\.\d+)*(\-(dev|patch|alpha|beta|RC)\d*)?$/', $ref['name'])) { | ||
$version = $ref['name']; | ||
} else { | ||
$version = 'dev-' . $ref['name']; | ||
} | ||
|
||
if (($data = $fetch_composer($project, $ref['commit']['id'])) !== false) { | ||
$data['version'] = $version; | ||
$data['source'] = [ | ||
'url' => $project[method . '_url_to_repo'], | ||
'type' => 'git', | ||
'reference' => $ref['commit']['id'], | ||
]; | ||
|
||
$ref_cache[$ref_key] = [$version => $data]; | ||
} else { | ||
$ref_cache[$ref_key] = []; | ||
} | ||
} | ||
|
||
return $ref_cache[$ref_key]; | ||
}; | ||
|
||
/** | ||
* Retrieves some information about a project for all refs | ||
* @param array $project | ||
* @return array Same as $fetch_ref, but for all refs | ||
*/ | ||
$fetch_refs = function($project) use ($fetch_ref, $repos) { | ||
$datas = array(); | ||
try { | ||
foreach (array_merge($repos->branches($project['id']), $repos->tags($project['id'])) as $ref) { | ||
foreach ($fetch_ref($project, $ref) as $version => $data) { | ||
$datas[$version] = $data; | ||
} | ||
} | ||
} catch (RuntimeException $e) { | ||
// The repo has no commits — skipping it. | ||
} | ||
|
||
return $datas; | ||
}; | ||
|
||
/** | ||
* Caching layer on top of $fetch_refs | ||
* Uses last_activity_at from the $project array, so no invalidation is needed | ||
* | ||
* @param array $project | ||
* @return array Same as $fetch_refs | ||
*/ | ||
$load_data = function($project) use ($fetch_refs) { | ||
$file = __DIR__ . "/../cache/{$project['path_with_namespace']}.json"; | ||
$mtime = strtotime($project['last_activity_at']); | ||
|
||
if (!is_dir(dirname($file))) { | ||
mkdir(dirname($file), 0777, true); | ||
} | ||
|
||
if (file_exists($file) && filemtime($file) >= $mtime) { | ||
if (filesize($file) > 0) { | ||
return json_decode(file_get_contents($file)); | ||
} else { | ||
return false; | ||
} | ||
} elseif ($data = $fetch_refs($project)) { | ||
file_put_contents($file, json_encode($data)); | ||
touch($file, $mtime); | ||
|
||
return $data; | ||
} else { | ||
$f = fopen($file, 'w'); | ||
fclose($f); | ||
touch($file, $mtime); | ||
|
||
return false; | ||
} | ||
}; | ||
|
||
/** | ||
* Determine the name to use for the package. | ||
* | ||
* @param array $project | ||
* @return string The name of the project | ||
*/ | ||
$get_package_name = function($project) use ($allow_package_name_mismatches, $fetch_ref, $repos) { | ||
if ($allow_package_name_mismatches) { | ||
$ref = $fetch_ref($project, $repos->branch($project['id'], $project['default_branch'])); | ||
return reset($ref)['name']; | ||
} | ||
|
||
return $project['path_with_namespace']; | ||
}; | ||
|
||
// Load projects | ||
$all_projects = array(); | ||
$mtime = 0; | ||
if (!empty($confs['groups'])) { | ||
// We have to get projects from specifics groups | ||
foreach ($groups->all(array('page' => 1, 'per_page' => 100)) as $group) { | ||
if (!in_array($group['name'], $confs['groups'], true)) { | ||
continue; | ||
} | ||
for ($page = 1; count($p = $groups->projects($group['id'], array('page' => $page, 'per_page' => 100))); $page++) { | ||
foreach ($p as $project) { | ||
$all_projects[] = $project; | ||
$mtime = max($mtime, strtotime($project['last_activity_at'])); | ||
} | ||
} | ||
} | ||
} else { | ||
// We have to get all accessible projects | ||
$me = $client->api('users')->me(); | ||
for ($page = 1; count($p = $projects->all(array('page' => $page, 'per_page' => 100))); $page++) { | ||
foreach ($p as $project) { | ||
$all_projects[] = $project; | ||
$mtime = max($mtime, strtotime($project['last_activity_at'])); | ||
} | ||
} | ||
try { | ||
$confs = (new Config())->getConfs(); | ||
$a = new Auth(); | ||
$a->setConfig($confs); | ||
$a->auth(); | ||
$Cr = new RegistryBuilder(); | ||
$Cr->setConfig($confs); | ||
$Cr->outputFile(); | ||
} catch (\Exception $ex) { | ||
print $ex; | ||
} | ||
|
||
// Regenerate packages_file is needed | ||
if (!file_exists($packages_file) || filemtime($packages_file) < $mtime) { | ||
$packages = array(); | ||
foreach ($all_projects as $project) { | ||
if (($package = $load_data($project)) && ($package_name = $get_package_name($project))) { | ||
$packages[$package_name] = $package; | ||
} | ||
} | ||
if ( file_exists( $static_file ) ) { | ||
$static_packages = json_decode( file_get_contents( $static_file ) ); | ||
foreach ( $static_packages as $name => $package ) { | ||
foreach ( $package as $version => $root ) { | ||
if ( isset( $root->extra ) ) { | ||
$source = '_source'; | ||
while ( isset( $root->extra->{$source} ) ) { | ||
$source = '_' . $source; | ||
} | ||
$root->extra->{$source} = 'static'; | ||
} | ||
else { | ||
$root->extra = array( | ||
'_source' => 'static', | ||
); | ||
} | ||
} | ||
$packages[$name] = $package; | ||
} | ||
} | ||
$data = json_encode(array( | ||
'packages' => array_filter($packages), | ||
)); | ||
|
||
file_put_contents($packages_file, $data); | ||
} | ||
|
||
$outputFile($packages_file); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<?php | ||
namespace GitlabComposer; | ||
require __DIR__ . '/../vendor/autoload.php'; | ||
$confs = (new Config())->getConfs(); | ||
$a=new AuthWebhook(); | ||
$a->setConfig($confs); | ||
$a->auth(); | ||
$Cr=new RegistryBuilder(); | ||
$Cr->setConfig($confs); | ||
$Cr->update(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
<?php | ||
/** | ||
* Created by PhpStorm. | ||
* User: keywan | ||
* Date: 30.07.18 | ||
* Time: 08:49 | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: I'd prefer if we didn't have editor-specific comments. I'm definitely fine with an If you're to include an |
||
|
||
namespace GitlabComposer; | ||
|
||
|
||
class Auth | ||
{ | ||
protected $confs; | ||
|
||
public function getAllowedIps(){ | ||
return $this->confs['allowed_client_ips']; | ||
} | ||
|
||
public function auth(){ | ||
$ips = $this->getAllowedIps(); | ||
if ($ips) { | ||
if (!isset($_SERVER['REMOTE_ADDR'])){ | ||
return true; | ||
} | ||
$REMOTE_ADDR = $_SERVER['REMOTE_ADDR']; | ||
if (in_array($REMOTE_ADDR, $ips)){ | ||
return true; | ||
} | ||
exit($REMOTE_ADDR . ' is not allowed to access'); | ||
} | ||
} | ||
|
||
public function setConfig($confs){ | ||
$this->confs = $confs; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
<?php | ||
/** | ||
* This Software is the property of OXID eSales and is protected | ||
* by copyright law - it is NOT Freeware. | ||
* | ||
* Any unauthorized use of this software without a valid license key | ||
* is a violation of the license agreement and will be prosecuted by | ||
* civil and criminal law. | ||
* | ||
* @author OXID Professional services | ||
* @link http://www.oxid-esales.com | ||
* @copyright (C) OXID eSales AG | ||
* Created at 7/30/18 1:08 PM by Keywan Ghadami | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a bit concerned by the Copyright notice here. Especially the "it is NOT Freeware" mention. |
||
|
||
namespace GitlabComposer; | ||
|
||
|
||
class AuthWebhook extends Auth | ||
{ | ||
protected $data; | ||
|
||
public function getAllowedIps(){ | ||
return $this->confs['allowed_webhook_ips']; | ||
} | ||
|
||
|
||
public function auth(){ | ||
if (!$this->confs['webhook_token']) { | ||
http_response_code(500); | ||
exit("webhook_token is not configured in gitlab.ini, please add it to the composer-gitlab config file"); | ||
} | ||
if (!$_SERVER['HTTP_X_GITLAB_TOKEN'] == $this->confs['webhook_token']){ | ||
http_response_code(403); | ||
exit("X-Gitlab-Token is not allowed to access"); | ||
} | ||
return parent::auth(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: I'm not sure about the point of this empty file... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If IP restrictions apply. This will require having the Gitlab server in the set of allowed IPs.
This might become an issue with complex setups.
What do you think about separating the IP restriction config for hooks and regular queries?