Last active
December 16, 2023 22:03
-
-
Save AnonyMouse-Box/3f217cc8b1507ba6efcea336a037131a to your computer and use it in GitHub Desktop.
PHP Github Webhook Pull Request Handler
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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