Skip to content

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,10 @@
"require": {
"m4tthumphrey/php-gitlab-api": "^9.0",
"php-http/guzzle6-adapter": "^1.1"
},
"autoload": {
"psr-4": {
"GitlabComposer\\": "src"
}
}
}
2 changes: 2 additions & 0 deletions confs/.htaccess
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Satisfy All
Deny from all
16 changes: 16 additions & 0 deletions confs/samples/gitlab.ini
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@
endpoint="http://gitlab.example.com/api/v4/"
api_key="ASDFGHJKL12345678"
method="ssh"
;automaticly create a webhook in the project so every new tag will availible in the composer registry
;your API Key must belong to a user with maintaner rights or higher to use this setting
;create_webhook="true"

;You can set manually a URL if you do not want to relay to automaticlly getting the url during the call to package.json/package.php
;this must be set on public systems for security reason to avoid abuse usage of the webhook endpoint
;base_url="https://yourdomain/"

;ip white listing use it only as additional security layer
;allowed_client_ips[]=""
;allowed_client_ips[]=""
;allowed_webhook_ips[]=""
;allowed_webhook_ips[]=""

;MANDETORY setting to allow webhooks put a random secrete string here
;webhook_token=

; You can restrict to some gitlab groups:
;groups[]="one_group"
Expand Down
256 changes: 11 additions & 245 deletions htdocs/packages.php
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);
10 changes: 10 additions & 0 deletions htdocs/webhook.php
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();
Copy link
Contributor

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?

$Cr=new RegistryBuilder();
$Cr->setConfig($confs);
$Cr->update();
37 changes: 37 additions & 0 deletions src/Auth.php
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
*/
Copy link
Contributor

Choose a reason for hiding this comment

The 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 @author, but I'd prefer we didn't have the "Created by PhpStorm" comment. The date and time are somewhat irrelevant as well.

If you're to include an @author comment. Please include a link to your github profile or a public email address.


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;
}
}
39 changes: 39 additions & 0 deletions src/AuthWebhook.php
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
*/
Copy link
Contributor

Choose a reason for hiding this comment

The 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.
Since it's the only file with it, I believe you simply forgot it... But please, remove it.
As I mentioned earlier, I'm totally fine with the @author comment (although I'd prefer something that would allow to reach you easily via github or email).


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();
}
}
1 change: 1 addition & 0 deletions src/Builder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: I'm not sure about the point of this empty file...

Loading