This commit is contained in:
hhftechnologies 2024-11-19 23:05:28 +05:30
parent 4e72174670
commit e5fd11edbd
11 changed files with 3171 additions and 0 deletions

2017
Parsedown.php Normal file

File diff suppressed because it is too large Load diff

3
configuration.php Normal file
View file

@ -0,0 +1,3 @@
<?php
define('PATH', substr(__FILE__, 0, strrpos(__FILE__, '/')));

1
incidents/message.md Normal file
View file

@ -0,0 +1 @@
There are no active incidents.

222
index.php Normal file
View file

@ -0,0 +1,222 @@
<?php
// Start session for CSRF token management
session_start();
// CSRF Protection Functions
function generate_csrf_token() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function verify_csrf_token($token) {
// Check if token exists in session and matches submitted token
if (!isset($_SESSION['csrf_token']) ||
!isset($token) ||
!hash_equals($_SESSION['csrf_token'], $token)) {
// Log potential CSRF attempt
error_log('Potential CSRF attack detected');
// Destroy session and prevent further execution
session_destroy();
die('CSRF token validation failed');
}
// Regenerate token after successful validation for additional security
unset($_SESSION['csrf_token']);
generate_csrf_token();
}
// Configuration and include path handling
include('configuration.php');
include(PATH.'/Parsedown.php');
// Mandatory file and directory checks with CSRF protection
$requiredChecks = [
PATH.'/monitors.json' => '<h1>Missing monitors.json</h1>',
PATH.'/monitors' => '<h1>Missing monitors directory</h1>',
PATH.'/incidents' => '<h1>Missing incidents directory</h1>'
];
foreach ($requiredChecks as $path => $errorMessage) {
if (!file_exists($path)) {
die($errorMessage . '<p>See project documentation for more information.</p>');
}
}
// Check monitors directory writability
if (!is_writable(PATH.'/monitors')) {
die('<h1>Monitors directory is not writable</h1>');
}
?><!DOCTYPE html>
<html lang="en">
<head>
<title>HHF Website Monitor</title>
<meta charset="utf-8">
<meta name="theme-color" content="#212529">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="style.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.8.2/dist/chart.min.js" crossorigin="anonymous"></script>
<meta name="csrf-token" content="<?php echo generate_csrf_token(); ?>">
</head>
<body>
<main>
<h1>HHF Website Monitor</h1>
<?php
$incidents = array();
foreach(glob(PATH.'/incidents/*.md') as $incident_file) {
$incident_filename = str_replace(PATH.'/incidents/', '', $incident_file);
if(substr($incident_filename, 0, 6) == 'alert_') $incidents[$incident_filename] = 'alert';
else if(substr($incident_filename, 0, 7) == 'notice_') $incidents[$incident_filename] = 'notice';
else $incidents['post_'.$incident_filename] = 'post';
}
ksort($incidents);
foreach($incidents as $incident_filename => $class) {
if(substr($incident_filename, 0, 5) == 'post_') $incident_filename = substr($incident_filename, 5);
$Parsedown = new Parsedown();
echo '<div class="incident '.$class.'">';
echo $Parsedown->text(file_get_contents(PATH.'/incidents/'.$incident_filename));
echo '</div>';
}
$monitors = json_decode(file_get_contents(PATH.'/monitors.json'));
$i = 0;
foreach($monitors as $monitor => $url) {
$log = json_decode(file_get_contents(PATH.'/monitors/'.$monitor), TRUE);
$last = $log[array_key_last($log)];
if(is_numeric($last['response'])) {
$last = ' <span class="status">HTTP/1.1 '.$last['response'].'</span>';
$class = 'good';
$graph_color = '#51cf66';
}
else {
$last = ' <span class="status">HTTP/1.1 '.$last['response'].'</span>';
$class = 'bad';
$graph_color = '#fa5252';
}
echo '<div class="item"><h2><span class="'.$class.'">⬤</span> '.$monitor.$last.'</h2>';
if(file_exists(PATH.'/updates/'.$monitor.'.md')) {
$Parsedown = new Parsedown();
echo $Parsedown->text(file_get_contents(PATH.'/updates/'.$monitor.'.md'));
}
$labels = array();
$data = array();
$const = 'ctx'.$i;
$const_chart = $const.'_'.$const;
echo '<div class="bloops">';
// do we need bloop padding?
if(count($log) < 60) {
$diff = 59 - count($log);
for ($x = 0; $x <= $diff; $x++) {
echo '<span class="bloop" title="Not monitored"></span>';
}
}
foreach($log as $arr) {
$labels[] = "'".date("H:i", $arr['timestamp'])."'";
$data[] = @$arr['time'];
if(@$arr['time'] > 0) {
echo '<span class="bloop good" data-status="Up" data-time="'.date("H:i", $arr['timestamp']).'" data-response="'.$arr['response'].'" data-ms="'.$arr['time'].'" data-status="Up at '.date("H:i", $arr['timestamp']).'" title="Up at '.date("H:i", $arr['timestamp']).' ('.$arr['time'].' ms)"></span>';
}
else {
echo '<span class="bloop bad"></span>';
}
}
$min = min($data);
$max = max($data);
$diff = $max - $min;
$max += ($diff / 3);
$min -= ($diff / 3);
if($min < 0) $min = 0;
echo '</div>';
$chart_id = str_replace('.', '-', $monitor);
$labels = implode(', ', $labels);
$data = implode(', ', $data);
$out = <<<EOD
<canvas id="$chart_id" width="300" height="100"></canvas>
<script>
const $const = document.getElementById('$chart_id').getContext('2d');
const $const_chart = new Chart($const, {
type: 'line',
data: {
labels: [$labels],
datasets: [{
label: 'response time',
data: [$data],
backgroundColor: '$graph_color',
borderColor: '$graph_color',
borderWidth: 2,
tension: .4
}]
},
options: {
scales: {
y: {
beginAtZero: false,
min: $min,
max: $max,
grid: { display: false, drawBorder: false }
},
x: {
grid: { display: false, drawBorder: false }
}
},
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function(context) {
return context.parsed.y + ' ms';
}
}
}
}
}
});
</script>
EOD;
echo $out;
echo '</div>';
$i++;
}
?>
<footer>
<p>Website Monitor is an open source project. <a href="https://git.hhf.technology/hhf/HHFWebsiteMonitor.git">Download it on Forgejo</a>.</p>
</footer>
</main>
</body>
</html>

171
monitor.php Normal file
View file

@ -0,0 +1,171 @@
<?php
/**
* Secure Website Monitoring Script
*
* Monitors websites and logs their response times and status codes
* Recommended to run via cron job
*/
// Strict error reporting
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
// Validate and include configuration
$configPath = __DIR__ . '/configuration.php';
if (!file_exists($configPath)) {
error_log('Configuration file not found');
exit(1);
}
require_once $configPath;
// Validate PATH constant
if (!defined('PATH') || !is_dir(PATH)) {
error_log('Invalid monitoring directory');
exit(1);
}
// Secure monitors file path
$monitorsFile = PATH . '/monitors.json';
if (!file_exists($monitorsFile)) {
error_log('Monitors configuration file not found');
exit(1);
}
// Read and validate monitors
$monitorsJson = file_get_contents($monitorsFile);
$monitors = json_decode($monitorsJson, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($monitors)) {
error_log('Invalid monitors JSON');
exit(1);
}
// Maximum number of historical records to keep
const MAX_HISTORY_RECORDS = 60;
// Maximum execution time
set_time_limit(30);
/**
* Validate and sanitize URL
*
* @param string $url URL to validate
* @return string|false Sanitized URL or false if invalid
*/
function validateUrl($url) {
// Trim and validate URL
$url = trim($url);
// Check if URL is valid
if (!filter_var($url, FILTER_VALIDATE_URL)) {
return false;
}
// Additional URL schemes validation (optional)
$allowedSchemes = ['http', 'https'];
$urlParts = parse_url($url);
if (!isset($urlParts['scheme']) || !in_array($urlParts['scheme'], $allowedSchemes)) {
return false;
}
return $url;
}
/**
* Safely fetch website response
*
* @param string $url URL to monitor
* @return array Response data
*/
function fetchWebsiteResponse($url) {
$response_data = [
'timestamp' => time(),
'error' => null,
'time' => null,
'response' => null
];
// Validate URL
$sanitizedUrl = validateUrl($url);
if (!$sanitizedUrl) {
$response_data['error'] = 'Invalid URL';
return $response_data;
}
// Initialize cURL with safe options
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => $sanitizedUrl,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2
]);
// Execute request
$response = curl_exec($curl);
// Check for cURL errors
if ($response === false) {
$response_data['error'] = curl_error($curl);
} else {
// Get connection info
$info = curl_getinfo($curl);
$response_data['time'] = round($info['total_time_us'] / 1000, 2);
$response_data['response'] = $info['http_code'];
}
// Close cURL resource
curl_close($curl);
return $response_data;
}
/**
* Safely write monitor data
*
* @param string $name Monitor name
* @param array $newData New response data
*/
function writeMonitorData($name, $newData) {
// Validate monitor name (prevent path traversal)
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $name)) {
error_log("Invalid monitor name: $name");
return;
}
$monitorFile = PATH . '/monitors/' . $name;
// Read existing data
$data = [];
if (file_exists($monitorFile)) {
$fileContent = file_get_contents($monitorFile);
$data = json_decode($fileContent, true) ?: [];
}
// Merge and limit historical data
$data[] = $newData;
$data = array_slice($data, -MAX_HISTORY_RECORDS);
// Securely write file
$jsonData = json_encode($data, JSON_PRETTY_PRINT);
if (file_put_contents($monitorFile, $jsonData) === false) {
error_log("Failed to write monitor data for: $name");
}
}
// Main monitoring loop
foreach ($monitors as $name => $url) {
try {
$responseData = fetchWebsiteResponse($url);
writeMonitorData($name, $responseData);
} catch (Exception $e) {
error_log("Monitoring error for $name: " . $e->getMessage());
}
}

4
monitors.json Normal file
View file

@ -0,0 +1,4 @@
{
"forum.hhf.technology": "https:\/\/forum.hhf.technology",
"git.hhf.technology": "https:\/\/git.hhf.technology"
}

View file

@ -0,0 +1,302 @@
[
{
"timestamp": 1671749821,
"time": 4.192,
"response": 200
},
{
"timestamp": 1671749881,
"time": 6.308,
"response": 200
},
{
"timestamp": 1671749941,
"time": 4.439,
"response": 200
},
{
"timestamp": 1671750002,
"time": 4.309,
"response": 200
},
{
"timestamp": 1671750061,
"time": 4.195,
"response": 200
},
{
"timestamp": 1671750121,
"time": 4.228,
"response": 200
},
{
"timestamp": 1671750181,
"time": 4.366,
"response": 200
},
{
"timestamp": 1671750241,
"time": 4.25,
"response": 200
},
{
"timestamp": 1671750301,
"time": 4.322,
"response": 200
},
{
"timestamp": 1671750361,
"time": 4.349,
"response": 200
},
{
"timestamp": 1671750421,
"time": 4.231,
"response": 200
},
{
"timestamp": 1671750481,
"time": 5.795,
"response": 200
},
{
"timestamp": 1671750541,
"time": 4.355,
"response": 200
},
{
"timestamp": 1671750601,
"time": 4.224,
"response": 200
},
{
"timestamp": 1671750661,
"time": 4.213,
"response": 200
},
{
"timestamp": 1671750721,
"time": 4.274,
"response": 200
},
{
"timestamp": 1671750781,
"time": 4.258,
"response": 200
},
{
"timestamp": 1671750841,
"time": 4.24,
"response": 200
},
{
"timestamp": 1671750901,
"time": 4.22,
"response": 200
},
{
"timestamp": 1671750961,
"time": 4.23,
"response": 200
},
{
"timestamp": 1671751021,
"time": 4.24,
"response": 200
},
{
"timestamp": 1671751081,
"time": 4.225,
"response": 200
},
{
"timestamp": 1671751141,
"time": 4.199,
"response": 200
},
{
"timestamp": 1671751201,
"time": 4.263,
"response": 200
},
{
"timestamp": 1671751261,
"time": 4.195,
"response": 200
},
{
"timestamp": 1671751321,
"time": 4.291,
"response": 200
},
{
"timestamp": 1671751381,
"time": 4.14,
"response": 200
},
{
"timestamp": 1671751441,
"time": 4.214,
"response": 200
},
{
"timestamp": 1671751501,
"time": 4.306,
"response": 200
},
{
"timestamp": 1671751561,
"time": 4.233,
"response": 200
},
{
"timestamp": 1671751621,
"time": 4.39,
"response": 200
},
{
"timestamp": 1671751681,
"time": 4.415,
"response": 200
},
{
"timestamp": 1671751741,
"time": 4.291,
"response": 200
},
{
"timestamp": 1671751801,
"time": 4.284,
"response": 200
},
{
"timestamp": 1671751861,
"time": 4.364,
"response": 200
},
{
"timestamp": 1671751921,
"time": 4.24,
"response": 200
},
{
"timestamp": 1671751981,
"time": 4.269,
"response": 200
},
{
"timestamp": 1671752041,
"time": 4.326,
"response": 200
},
{
"timestamp": 1671752101,
"time": 4.256,
"response": 200
},
{
"timestamp": 1671752161,
"time": 4.227,
"response": 200
},
{
"timestamp": 1671752221,
"time": 4.248,
"response": 200
},
{
"timestamp": 1671752281,
"time": 4.349,
"response": 200
},
{
"timestamp": 1671752341,
"time": 4.274,
"response": 200
},
{
"timestamp": 1671752401,
"time": 4.27,
"response": 200
},
{
"timestamp": 1671752461,
"time": 4.497,
"response": 200
},
{
"timestamp": 1671752521,
"time": 4.136,
"response": 200
},
{
"timestamp": 1671752581,
"time": 4.397,
"response": 200
},
{
"timestamp": 1671752641,
"time": 4.224,
"response": 200
},
{
"timestamp": 1671752701,
"time": 4.241,
"response": 200
},
{
"timestamp": 1671752761,
"time": 4.248,
"response": 200
},
{
"timestamp": 1671752821,
"time": 4.517,
"response": 200
},
{
"timestamp": 1671752881,
"time": 4.381,
"response": 200
},
{
"timestamp": 1671752941,
"time": 4.371,
"response": 200
},
{
"timestamp": 1671753001,
"time": 4.626,
"response": 200
},
{
"timestamp": 1671753061,
"time": 6.07,
"response": 200
},
{
"timestamp": 1671753121,
"time": 4.455,
"response": 200
},
{
"timestamp": 1671753181,
"time": 4.373,
"response": 200
},
{
"timestamp": 1671753241,
"time": 4.184,
"response": 200
},
{
"timestamp": 1671753301,
"time": 4.685,
"response": 200
},
{
"timestamp": 1671753361,
"time": 7.862,
"response": 200
}
]

302
monitors/git.hhf.technology Normal file
View file

@ -0,0 +1,302 @@
[
{
"timestamp": 1671749821,
"time": 225.4,
"response": 200
},
{
"timestamp": 1671749881,
"time": 215.243,
"response": 200
},
{
"timestamp": 1671749941,
"time": 232.023,
"response": 200
},
{
"timestamp": 1671750002,
"time": 219.797,
"response": 200
},
{
"timestamp": 1671750061,
"time": 228.624,
"response": 200
},
{
"timestamp": 1671750121,
"time": 226.033,
"response": 200
},
{
"timestamp": 1671750181,
"time": 224.71,
"response": 200
},
{
"timestamp": 1671750241,
"time": 239.184,
"response": 200
},
{
"timestamp": 1671750301,
"time": 223.836,
"response": 200
},
{
"timestamp": 1671750361,
"time": 224.607,
"response": 200
},
{
"timestamp": 1671750421,
"time": 220.908,
"response": 200
},
{
"timestamp": 1671750481,
"time": 229.849,
"response": 200
},
{
"timestamp": 1671750541,
"time": 238.873,
"response": 200
},
{
"timestamp": 1671750601,
"time": 224.467,
"response": 200
},
{
"timestamp": 1671750661,
"time": 215.798,
"response": 200
},
{
"timestamp": 1671750721,
"time": 225.466,
"response": 200
},
{
"timestamp": 1671750781,
"time": 209.352,
"response": 200
},
{
"timestamp": 1671750841,
"time": 225.638,
"response": 200
},
{
"timestamp": 1671750901,
"time": 228.026,
"response": 200
},
{
"timestamp": 1671750961,
"time": 215.496,
"response": 200
},
{
"timestamp": 1671751021,
"time": 238.666,
"response": 200
},
{
"timestamp": 1671751081,
"time": 218.908,
"response": 200
},
{
"timestamp": 1671751141,
"time": 216.086,
"response": 200
},
{
"timestamp": 1671751201,
"time": 225.955,
"response": 200
},
{
"timestamp": 1671751261,
"time": 224.078,
"response": 200
},
{
"timestamp": 1671751321,
"time": 223.235,
"response": 200
},
{
"timestamp": 1671751381,
"time": 226.129,
"response": 200
},
{
"timestamp": 1671751441,
"time": 228.306,
"response": 200
},
{
"timestamp": 1671751501,
"time": 212.357,
"response": 200
},
{
"timestamp": 1671751561,
"time": 239.809,
"response": 200
},
{
"timestamp": 1671751621,
"time": 237.583,
"response": 200
},
{
"timestamp": 1671751681,
"time": 224.485,
"response": 200
},
{
"timestamp": 1671751741,
"time": 235.199,
"response": 200
},
{
"timestamp": 1671751801,
"time": 239.203,
"response": 200
},
{
"timestamp": 1671751861,
"time": 239.08,
"response": 200
},
{
"timestamp": 1671751921,
"time": 221.216,
"response": 200
},
{
"timestamp": 1671751981,
"time": 220.381,
"response": 200
},
{
"timestamp": 1671752041,
"time": 219.535,
"response": 200
},
{
"timestamp": 1671752101,
"time": 218.568,
"response": 200
},
{
"timestamp": 1671752161,
"time": 225.099,
"response": 200
},
{
"timestamp": 1671752221,
"time": 233.544,
"response": 200
},
{
"timestamp": 1671752281,
"time": 231.748,
"response": 200
},
{
"timestamp": 1671752341,
"time": 219.454,
"response": 200
},
{
"timestamp": 1671752401,
"time": 236.159,
"response": 200
},
{
"timestamp": 1671752462,
"time": 220.944,
"response": 200
},
{
"timestamp": 1671752521,
"time": 209.714,
"response": 200
},
{
"timestamp": 1671752581,
"time": 240.137,
"response": 200
},
{
"timestamp": 1671752641,
"time": 239.22,
"response": 200
},
{
"timestamp": 1671752701,
"time": 238.772,
"response": 200
},
{
"timestamp": 1671752761,
"time": 223.05,
"response": 200
},
{
"timestamp": 1671752821,
"time": 223.467,
"response": 200
},
{
"timestamp": 1671752881,
"time": 230.855,
"response": 200
},
{
"timestamp": 1671752941,
"time": 223.755,
"response": 200
},
{
"timestamp": 1671753001,
"time": 218.684,
"response": 200
},
{
"timestamp": 1671753061,
"time": 223.834,
"response": 200
},
{
"timestamp": 1671753121,
"time": 219.454,
"response": 200
},
{
"timestamp": 1671753181,
"time": 215.982,
"response": 200
},
{
"timestamp": 1671753241,
"time": 225.577,
"response": 200
},
{
"timestamp": 1671753301,
"time": 227.389,
"response": 200
},
{
"timestamp": 1671753361,
"time": 225.435,
"response": 200
}
]

147
style.css Normal file
View file

@ -0,0 +1,147 @@
@import url('https://rsms.me/inter/inter.css');
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/fontawesome.min.css');
* {
text-rendering: optimizeLegibility;
}
body {
font-family: 'Inter', sans-serif;
background:#212529;
color: #f8f9fa;
font-size: 1.3em;
}
h2 {
margin: 0;
}
.incident {
background: #000;
padding: .1em 1em;
margin: 0 0 1em 0;
border-radius: .25em;
}
.post {
background: #495057;
color: #fff;
}
.notice {
background: #ffd43b;
color: #000;
}
.alert {
background: #c92a2a;
color: #fff;
}
.item p, .item p a {
margin: 0;
color: #aaa;
font-size: 95%;
}
p {
line-height: 150%;
}
canvas:hover {
cursor: crosshair;
}
main {
max-width: 40em;
margin: auto;
padding: 1em;
}
footer {
text-align: center;
margin: 0 0 3em 0;
color: #888;
font-size: 90%;
}
a:link, a:visited, a:hover, a:active {
color: #f8f9fa;
}
#status {
background: #343a40;
border-radius: .25em;
padding: .1em 1em;
}
#notice {
background: #ffec99;
color: #000;
border-radius: .25em;
padding: .1em 1em;
margin-top: 1em;
}
.status {
color: #868e96;
font-weight: normal;
font-size: 65%;
font-family: monospace;
padding-left: .7em;
}
.bloops {
margin: .5em 0;
}
.bloop {
background: #555;
width: calc((100% / 60) - 2px);
height: 30px;
border-radius: 5px;
margin-right: 2px;
display: inline-block;
}
.bloop:hover {
opacity: 0.5;
cursor: help;
}
.bloops p {
background: #495057;
padding: .5em;
border-radius: .25em;
}
.bloop.good { background: #51cf66; }
.bloop.bad { background: #fa5252; }
.bloop.meh { background: #ffd43b; }
.item {
margin: 3em 0;
}
.good { color: #51cf66; }
.bad { color: #fa5252; }
.meh { color: #ffd43b; }
.bloops {
position: relative;
}
.bloop_info {
position: absolute;
top: 2em; left: 0em;
background: #fff;
padding: 1em;
opacity: 1;
z-index: 100;
}
@media (max-width: 500px) {
body {
font-size: 1em;
}
}

View file

@ -0,0 +1 @@
hhf.technology, your go-to community for all things technology!.

View file

@ -0,0 +1 @@
HHF Technology Repository: A painless, self-hosted Git service.