Skip to content

Instantly share code, notes, and snippets.

@AnonyMouse-Box
Last active December 16, 2023 22:03
Show Gist options
  • Save AnonyMouse-Box/3f217cc8b1507ba6efcea336a037131a to your computer and use it in GitHub Desktop.
Save AnonyMouse-Box/3f217cc8b1507ba6efcea336a037131a to your computer and use it in GitHub Desktop.
PHP Github Webhook Pull Request Handler
<?php
echo <<<EOT
<!DOCTYPE html>
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.</p>
EOT;
$secret = ""; // Secret on webhook
$ipfile = ""; // Location to store IPs
$logfile = ""; // File to log output
$repo = ""; // Name of the repo
$from_branch = ""; // Name of the branch merged from
$to_branch = ""; // Name of the branch merged to
function parseIP($ipaddr) {
// Strip out the netmask, if there is one.
$cx = strpos($ipaddr, '/');
if ($cx) {
$subnet = (int)(substr($ipaddr, $cx+1));
$ipaddr = substr($ipaddr, 0, $cx);
}
else $subnet = null; // No netmask present
// Convert address to packed format
$addr = inet_pton($ipaddr);
// Convert the netmask
if (is_integer($subnet)) {
// Maximum netmask length = same as packed address
$len = 8*strlen($addr);
if ($subnet > $len) $subnet = $len;
// Create a hex expression of the subnet mask
$mask = str_repeat('f', $subnet>>2);
switch($subnet & 3)
{
case 3: $mask .= 'e'; break;
case 2: $mask .= 'c'; break;
case 1: $mask .= '8'; break;
}
$mask = str_pad($mask, $len>>2, '0');
// Packed representation of netmask
$mask = pack('H*', $mask);
// Enforce network address
$addr = $addr & $mask;
}
$network = ["network" => $addr, "subnet" => $mask];
return $network;
}
// Set up CuRL and pull github addresses
$agent = "Googlebot/2.1 (+http://www.google.com/bot.html)";
$ch = curl_init("https://api.github.com/meta");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
curl_setopt($ch, CURLOPT_USERAGENT, $agent);
curl_setopt($ch, CURLOPT_FAILONERROR, true);
$github = curl_exec($ch);
$http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Strip out only the hooks and add to a file for storage
if ($http_status == 200) {
file_put_contents($ipfile , implode("\n", json_decode($github, true)["hooks"]));
}
// Pull header information and set up check variables
$github = explode("\n", file_get_contents($ipfile));
$matches = false;
$allowed = false;
$headers = apache_request_headers();
$data = file_get_contents('php://input');
// Find the originating IP to use for the request
if (@$headers["X-Forwarded-For"]) {
$ips = explode(",",$headers["X-Forwarded-For"]);
$ip = $ips[0];
} else {
$ip = $_SERVER['REMOTE_ADDR'];
}
// Check the signature hashes match
if (hash_equals("sha1=".hash_hmac('sha1', $data, $secret), $headers["X-Hub-Signature"]) and hash_equals("sha256=".hash_hmac('sha256', $data, $secret), $headers["X-Hub-Signature-256"])) {
$matches = true;
}
// Perform firewall ANDing match to validate IP
$request = parseIP($ip);
foreach ($github as $valid_ip) {
$allow = parseIP($valid_ip);
if (!is_null($allow["subnet"])) {
$ip_match = $request["network"] & $allow["subnet"];
} else {
$ip_match = $request["network"];
}
if (strcmp($allow["network"], $ip_match) === 0) {
$allowed = true;
break;
}
}
// If either match fails pretend to not exist
if (!$allowed or !$matches) {
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
echo "</body>\n</html>";
exit;
}
flush();
// Initialize log file
$log = "####### " . date('Y-m-d H:i:s') . " #######\n";
// Check a pull request from development to main was successfully merged not simply closed.
$payload = json_decode($data, true);
if ($payload["action"] === "closed"
and $payload["pull_request"]["merged"] === true
and $payload["pull_request"]["head"]["label"] === $repo . ":" . $from_branch
and $payload["pull_request"]["base"]["label"] === $repo . ":" . $to_branch) {
$log .= "Verified\n";
// Leave a description of why the request was invalid
} else {
$log .= "Action: " . $payload["action"] . "\n";
$log .= "From: " . $payload["pull_request"]["head"]["label"] . "\n";
$log .= "To: " . $payload["pull_request"]["base"]["label"] . "\n";
$merged = $payload["pull_request"]["merged"] ? 'true' : 'false';
$log .= "Merged: " . $merged . "\n";
$log .= "Invalid request... Ignoring.\n";
}
// Clean up and add to deploy.log
$log .= "\n\n";
file_put_contents($logfile, $log, FILE_APPEND);
?>
</body>
</html>
@AnonyMouse-Box
Copy link
Author

Will appear to be almost invisible to a casual observer, webhook will receive a 404 if unsuccessful, but a 200 if validated to be from the correct webhook

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment