228 lines
6.4 KiB
PHP
228 lines
6.4 KiB
PHP
<?php
|
|
|
|
function error($message, $code = 500) {
|
|
http_response_code($code);
|
|
// Echo to STDOUT, not STDERR as some servers will hide STDERR for security reasons
|
|
echo($message);
|
|
exit();
|
|
}
|
|
|
|
// extract_payload()
|
|
// Find the JSON payload from the POST request
|
|
function extract_payload() {
|
|
// check for POST request
|
|
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
|
|
error('FAILED - not POST - '. $_SERVER['REQUEST_METHOD']);
|
|
}
|
|
// get content type
|
|
$content_type = isset($_SERVER['CONTENT_TYPE']) ? strtolower(trim($_SERVER['CONTENT_TYPE'])) : '';
|
|
if ($content_type != 'application/json') {
|
|
error('FAILED - not application/json - '. $content_type);
|
|
}
|
|
// get payload
|
|
$payload = file_get_contents("php://input");
|
|
if (empty($payload)) {
|
|
error('FAILED - no payload');
|
|
}
|
|
return $payload;
|
|
|
|
}
|
|
|
|
// json_to_array(payload)
|
|
// payload: JSON string
|
|
function json_to_array($payload) {
|
|
// convert json to array
|
|
$json = json_decode($payload, true);
|
|
// check for json decode errors
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
error('FAILED - json decode - '. json_last_error());
|
|
}
|
|
return $json;
|
|
}
|
|
|
|
// Find header value or die
|
|
function extract_header($header) {
|
|
// PHP embedded server prepends HTTP_ to the header name. WTF?
|
|
$value = isset($_SERVER[$header]) ? $_SERVER[$header]
|
|
: (isset($_SERVER['HTTP_'.$header]) ? $_SERVER['HTTP_'.$header]
|
|
: '');
|
|
|
|
if (empty($value)) {
|
|
error('FAILED - header signature '.$header.' missing');
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
function verify_claim(string $payload, string $claim, string $id, string $validator) {
|
|
$whck = find_whck();
|
|
switch ($validator) {
|
|
case "hmac-sha256":
|
|
$kind = "hmac-sha256";
|
|
break;
|
|
case "token":
|
|
$kind = "token";
|
|
break;
|
|
default:
|
|
error("Programming error", 500);
|
|
break;
|
|
}
|
|
|
|
|
|
$descriptors = [ array("pipe", "r"), array("pipe", "w"), array("pipe", "w") ];
|
|
//$cmd = $whck." " . $kind . " ".escapeshellarg($id) . " " . escapeshellarg($claim);
|
|
$cmd = [ $whck, $kind, $id, $claim ];
|
|
var_dump($cmd);
|
|
//echo "$cmd";
|
|
$proc = proc_open($cmd, $descriptors, $pipes);
|
|
|
|
if (is_resource($proc)) {
|
|
// Write payload to STDIN of whck
|
|
fwrite($pipes[0], $payload);
|
|
fclose($pipes[0]);
|
|
$stdout = stream_get_contents($pipes[1]);
|
|
fclose($pipes[1]);
|
|
$stderr = stream_get_contents($pipes[2]);
|
|
} else {
|
|
error("FAILED - could not start whck", 500);
|
|
}
|
|
|
|
$status = proc_close($proc);
|
|
if ($status != 0) {
|
|
error("Process failed due to error. STDOUT:\n$stdout\n\nSTDERR:\n$stderr", 500);
|
|
}
|
|
}
|
|
|
|
// Decide which whck executable to use
|
|
function find_whck() {
|
|
// BUG: https://bugs.php.net/bug.php?id=77782
|
|
// $_ENV['whck'] and getenv("whck") are empty, but getenv()["whck"] works!
|
|
// NOTE: We use is_file not file_exists() because there may be a whck/ folder
|
|
// In that case, we check for bin/whck
|
|
return isset(getenv()['WHCK']) ? getenv()['WHCK'] // from $ENV
|
|
: (is_file('whck') ? './whck' // from current directory
|
|
: (is_file('bin/whck') ? 'bin/whck' : 'whck'));
|
|
}
|
|
|
|
//enum Validator {
|
|
// case HMAC_SHA256;
|
|
// case Token;
|
|
//}
|
|
|
|
class JSONWebhook {
|
|
function __construct(string $validator) {
|
|
$this->validator = $validator;
|
|
$this->plaintext = extract_payload();
|
|
$this->data = json_to_array($this->plaintext);
|
|
}
|
|
|
|
function findId() {
|
|
return base64_encode($this->repo);
|
|
}
|
|
|
|
function verify() {
|
|
verify_claim($this->plaintext, $this->claim, $this->id, $this->validator);
|
|
}
|
|
}
|
|
|
|
class GiteaWebhook extends JSONWebhook {
|
|
function __construct() {
|
|
parent::__construct("hmac-sha256");
|
|
$this->repo = $this->findRepo();
|
|
$this->id = $this->findId();
|
|
$this->claim = extract_header("HTTP_X_GITEA_SIGNATURE");
|
|
}
|
|
|
|
function findRepo() {
|
|
$repo_url = isset($this->data["repository"]["html_url"]) ?
|
|
$this->data["repository"]["html_url"] : "";
|
|
if (empty($repo_url)) {
|
|
error('Could not find Gitea repository URL');
|
|
}
|
|
return $repo_url;
|
|
}
|
|
}
|
|
|
|
class GitlabWebhook extends JSONWebhook {
|
|
function __construct() {
|
|
parent::__construct("token");
|
|
$this->repo = $this->findRepo();
|
|
$this->id = $this->findId();
|
|
$this->claim = extract_header("HTTP_X_GITLAB_TOKEN");
|
|
}
|
|
|
|
function findRepo() {
|
|
$repo_url = isset($this->data["project"]["git_http_url"]) ?
|
|
$this->data["project"]["git_http_url"] : "";
|
|
if (empty($repo_url)) {
|
|
error('Could not find Gitlab repository URL');
|
|
}
|
|
return $repo_url;
|
|
}
|
|
}
|
|
|
|
|
|
class GithubWebhook extends JSONWebhook {
|
|
function __construct() {
|
|
parent::__construct("hmac-sha256");
|
|
$this->repo = $this->findRepo();
|
|
$this->id = $this->findId();
|
|
$this->claim = extract_header("HTTP_X_HUB_SIGNATURE");
|
|
}
|
|
|
|
function findRepo() {
|
|
$repo_url = isset($this->data["repository"]["html_url"]) ?
|
|
$this->data["repository"]["html_url"] : "";
|
|
if (empty($repo_url)) {
|
|
error('Could not find Github repository URL');
|
|
}
|
|
return $repo_url;
|
|
}
|
|
}
|
|
|
|
function action() {
|
|
if (!isset($_GET['action'])) {
|
|
error("You need to specify an action (gitea, gitlab) like this: ?action=gitea", 404);
|
|
}
|
|
|
|
switch($_GET['action']) {
|
|
case 'github':
|
|
$webhook = new GithubWebhook();
|
|
$webhook->verify();
|
|
break;
|
|
case 'gitea':
|
|
$webhook = new GiteaWebhook();
|
|
$webhook->verify();
|
|
break;
|
|
case 'gitlab':
|
|
$webhook = new GitlabWebhook();
|
|
$webhook->verify();
|
|
break;
|
|
default:
|
|
error("Unrecognized action: ".$_GET['action'], 400);
|
|
}
|
|
}
|
|
|
|
action();
|
|
echo("OK");
|
|
|
|
|
|
// verify_secret($repo_url, $secret)
|
|
// Verify a secret for the given URL. Useful when we don't have permission to read the secret.
|
|
function verify_secret($repo_url, $secret) {
|
|
$whck = find_whck();
|
|
$repo = escapeshellarg($repo_url);
|
|
$secret = escapeshellarg($secret);
|
|
$lines = [];
|
|
$status = NULL;
|
|
$secret = exec($forgehook." verify ".$repo." ".$secret, $lines, $status);
|
|
|
|
if (($secret == NULL) or ($status != 0)) {
|
|
echo "$secret";
|
|
error("Incorrect secret for \"".$repo."\": \"".$secret."\"", 403);
|
|
}
|
|
}
|
|
|
|
|
|
?>
|