Merge pull request #7 from hybula/main

Release 1.1.0
This commit is contained in:
Tamer 2022-11-23 22:36:39 +01:00 committed by GitHub
commit 3f085f9b03
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 612 additions and 186 deletions

4
.gitignore vendored
View file

@ -1,3 +1,3 @@
.idea
config.php
/config.php
!php-fpm/src/config.php

View file

@ -6,11 +6,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.0.1] - 19-04-2022
## 1.1.0 - 2022-11-23
### Fixed
- Changelog date format adjusted to ISO 8601.
- Improvements on MTR live output thanks to @MarcHagen.
- Minor improvements in backend thanks to @MarcHagen.
- Improved Docker support thanks to @MarcHagen.
- Updated README.md about POSIX installation.
- Updated Bootstrap to version 5.2.3
### Added
- A way to add custom <head> content, in the future this will replace the custom CSS feature.
- Upgrade tips to README.md.
## [1.0.1] - 2022-04-19
### Fixed
- Ping6/Traceroute6/MTR6 validation when using hostnames (PR #2).
## [1.0.0] - 14-04-2022
## [1.0.0] - 2022-04-14
### Fixed
- Undefined warning when refreshing a page after execution.
- Traceroute error when there is no PID found.
@ -19,6 +32,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added Docker support.
- This CHANGELOG.md to track changes.
## [0.1.0] - 17-01-2022
## [0.1.0] - 2022-01-17
### Changed
- First release.

View file

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/**
* Hybula Looking Glass
*
@ -10,15 +10,28 @@
* @since File available since release 0.1
* @link https://github.com/hybula/lookingglass
*/
declare(strict_types=1);
namespace Hybula;
class LookingGlass
{
public static $targetHost;
public static $targetType;
public const IPV4 = 'ipv4';
public const IPV6 = 'ipv6';
public const SESSION_TARGET_HOST = 'target_host';
public const SESSION_TARGET_METHOD = 'target_method';
public const SESSION_TOS_CHECKED = 'tos_checked';
public const SESSION_CALL_BACKEND = 'call_backend';
public const SESSION_ERROR_MESSAGE = 'error_message';
public const SESSION_CSRF = 'CSRF';
public const METHOD_PING = 'ping';
public const METHOD_PING6 = 'ping6';
public const METHOD_MTR = 'mtr';
public const METHOD_MTR6 = 'mtr6';
public const METHOD_TRACEROUTE = 'traceroute';
public const METHOD_TRACEROUTE6 = 'traceroute6';
private const MTR_COUNT = 10;
/**
* Validates the config.php file for required constants.
@ -27,6 +40,7 @@ class LookingGlass
*/
public static function validateConfig(): void
{
//@formatter:off
if (!defined('LG_TITLE')) die('LG_TITLE not found in config.php');
if (!defined('LG_LOGO')) die('LG_LOGO not found in config.php');
if (!defined('LG_LOGO_URL')) die('LG_LOGO_URL not found in config.php');
@ -38,6 +52,7 @@ class LookingGlass
if (!defined('LG_CUSTOM_HTML')) die('LG_CUSTOM_HTML not found in config.php');
if (!defined('LG_CUSTOM_PHP')) die('LG_CUSTOM_PHP not found in config.php');
if (!defined('LG_LOCATION')) die('LG_LOCATION not found in config.php');
if (!defined('LG_MAPS_QUERY')) die('LG_MAPS_QUERY not found in config.php');
if (!defined('LG_FACILITY')) die('LG_FACILITY not found in config.php');
if (!defined('LG_FACILITY_URL')) die('LG_FACILITY_URL not found in config.php');
if (!defined('LG_IPV4')) die('LG_IPV4 not found in config.php');
@ -51,6 +66,7 @@ class LookingGlass
if (!defined('LG_SPEEDTEST_CMD_OUTGOING')) die('LG_SPEEDTEST_CMD_OUTGOING not found in config.php');
if (!defined('LG_SPEEDTEST_FILES')) die('LG_SPEEDTEST_FILES not found in config.php');
if (!defined('LG_TERMS')) die('LG_TERMS not found in config.php');
//@formatter:on
}
/**
@ -105,17 +121,20 @@ class LookingGlass
if (!substr_count($host, '.')) {
return '';
}
if (filter_var('https://' . $host, FILTER_VALIDATE_URL)) {
if ($host = parse_url('https://' . $host, PHP_URL_HOST)) {
if ($type == 'ipv4' && isset(dns_get_record($host, DNS_A)[0]['ip'])) {
if ($type === self::IPV4 && isset(dns_get_record($host, DNS_A)[0]['ip'])) {
return $host;
}
if ($type == 'ipv6' && isset(dns_get_record($host, DNS_AAAA)[0]['ipv6'])) {
if ($type === self::IPV6 && isset(dns_get_record($host, DNS_AAAA)[0]['ipv6'])) {
return $host;
}
return '';
}
}
return '';
}
@ -128,6 +147,10 @@ class LookingGlass
*/
public static function detectIpAddress(): string
{
if (php_sapi_name() === 'cli') {
return '127.0.0.1';
}
if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && filter_var($_SERVER['HTTP_X_FORWARDED_FOR'], FILTER_VALIDATE_IP)) {
return $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
@ -167,7 +190,7 @@ class LookingGlass
*/
public static function mtr(string $host): bool
{
return self::procExecute('mtr -4 --report --report-wide', $host);
return self::procExecute('mtr --raw -n -4 -c ' . self::MTR_COUNT, $host);
}
/**
@ -178,7 +201,7 @@ class LookingGlass
*/
public static function mtr6(string $host): bool
{
return self::procExecute('mtr -6 --report --report-wide', $host);
return self::procExecute('mtr --raw -n -6 -c ' . self::MTR_COUNT, $host);
}
/**
@ -219,11 +242,11 @@ class LookingGlass
private static function procExecute(string $cmd, string $host, int $failCount = 2): bool
{
// define output pipes
$spec = array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("pipe", "w")
);
$spec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w']
];
// sanitize + remove single quotes
$host = str_replace('\'', '', filter_var($host, FILTER_SANITIZE_URL));
@ -238,16 +261,17 @@ class LookingGlass
// check for mtr/traceroute
if (strpos($cmd, 'mtr') !== false) {
$type = 'mtr';
$parser = new Parser();
} elseif (strpos($cmd, 'traceroute') !== false) {
$type = 'traceroute';
} else {
$type = '';
}
$fail = 0;
$match = 0;
$fail = 0;
$match = 0;
$traceCount = 0;
$lastFail = 'start';
$lastFail = 'start';
// iterate stdout
while (($str = fgets($pipes[1], 4096)) != null) {
// check for output buffer
@ -260,12 +284,14 @@ class LookingGlass
// correct output for mtr
if ($type === 'mtr') {
if ($match < 10 && preg_match('/^[0-9]\. /', $str, $string)) {
$str = preg_replace('/^[0-9]\. /', '&nbsp;&nbsp;' . $string[0], $str);
$match++;
} else {
$str = preg_replace('/^[0-9]{2}\. /', '&nbsp;' . substr($str, 0, 4), $str);
}
// correct output for mtr
$parser->update($str);
echo '---' . PHP_EOL . $parser->__toString() . PHP_EOL . str_pad('', 4096) . PHP_EOL;
// flush output buffering
@ob_flush();
flush();
continue;
}
// correct output for traceroute
elseif ($type === 'traceroute') {
@ -306,7 +332,7 @@ class LookingGlass
}
$status = proc_get_status($process);
if ($status['running'] == true) {
if ($status['running']) {
// close pipes that are still open
foreach ($pipes as $pipe) {
fclose($pipe);
@ -328,3 +354,189 @@ class LookingGlass
return true;
}
}
class Hop
{
/** @var int */
public $idx;
/** @var string */
public $asn = '';
/** @var float */
public $avg = 0.0;
/** @var int */
public $loss = 0;
/** @var float */
public $stdev = 0.0;
/** @var int */
public $sent = 0;
/** @var int */
public $recieved = 0;
/** @var float */
public $last = 0.0;
/** @var float */
public $best = 0.0;
/** @var float */
public $worst = 0.0;
/** @var string[] */
public $ips = [];
/** @var string[] */
public $hosts = [];
/** @var float[] */
public $timings = [];
}
class RawHop
{
/** @var string */
public $dataType;
/** @var int */
public $idx;
/** @var string */
public $value;
}
class Parser
{
/** @var Hop[] */
protected $hopsCollection = [];
/** @var int */
private $hopCount = 0;
/** @var int */
private $outputWidth = 38;
public function __construct()
{
putenv('RES_OPTIONS=retrans:1 retry:1 timeout:1 attempts:1');
}
public function __toString(): string
{
$str = '';
foreach ($this->hopsCollection as $index => $hop) {
$host = $hop->hosts[0] ?? $hop->ips[0] ?? '???';
if (strlen($host) > $this->outputWidth) {
$this->outputWidth = strlen($host);
}
$hop->recieved = count($hop->timings);
if (count($hop->timings)) {
$hop->last = $hop->timings[count($hop->timings) - 1];
$hop->best = $hop->timings[0];
$hop->worst = $hop->timings[0];
$hop->avg = array_sum($hop->timings) / count($hop->timings);
}
if (count($hop->timings) > 1) {
$hop->stdev = $this->stDev($hop->timings);
}
foreach ($hop->timings as $time) {
if ($hop->best > $time) {
$hop->best = $time;
}
if ($hop->worst < $time) {
$hop->worst = $time;
}
}
$hop->loss = $hop->sent ? (100 * ($hop->sent - $hop->recieved)) / $hop->sent : 100;
$str = sprintf(
"%s%2d.|-- %s%3d.0%% %3d %5.1f %5.1f %5.1f %5.1f %5.1f\n",
$str,
$index,
str_pad($host, $this->outputWidth + 3, ' ', STR_PAD_RIGHT),
$hop->loss,
$hop->sent,
$hop->last,
$hop->avg,
$hop->best,
$hop->worst,
$hop->stdev
);
}
return sprintf(" Host%sLoss%% Snt Last Avg Best Wrst StDev\n%s", str_pad('', $this->outputWidth + 7, ' ', STR_PAD_RIGHT), $str);
}
private function stDev(array $array): float
{
$sdSquare = function ($x, $mean) {
return pow($x - $mean, 2);
};
// square root of sum of squares devided by N-1
return sqrt(array_sum(array_map($sdSquare, $array, array_fill(0, count($array), (array_sum($array) / count($array))))) / (count($array) - 1));
}
public function update($rawMtrInput)
{
//Store each line of output in rawhop structure
$things = explode(' ', $rawMtrInput);
if (count($things) !== 3 && (count($things) !== 4 && $things[0] === 'p')) {
return;
}
$rawHop = new RawHop();
$rawHop->dataType = $things[0];
$rawHop->idx = (int)$things[1];
$rawHop->value = $things[2];
if ($this->hopCount < $rawHop->idx + 1) {
$this->hopCount = $rawHop->idx + 1;
}
if (!isset($this->hopsCollection[$rawHop->idx])) {
$this->hopsCollection[$rawHop->idx] = new Hop();
}
$hop = $this->hopsCollection[$rawHop->idx];
$hop->idx = $rawHop->idx;
switch ($rawHop->dataType) {
case 'h':
$hop->ips[] = $rawHop->value;
$hop->hosts[] = gethostbyaddr($rawHop->value) ? : null;
break;
case 'd':
//Not entirely sure if multiple IPs. Better use -n in mtr and resolve later in summarize.
//out.Hops[data.idx].Host = append(out.Hops[data.idx].Host, data.value)
break;
case 'p':
$hop->sent++;
$hop->timings[] = (float)$rawHop->value / 1000;
break;
}
$this->hopsCollection[$rawHop->idx] = $hop;
$this->filterLastDupeHop();
}
// Function to calculate standard deviation (uses sd_square)
private function filterLastDupeHop()
{
// filter dupe last hop
$finalIdx = 0;
$previousIp = '';
foreach ($this->hopsCollection as $key => $hop) {
if (count($hop->ips) && $hop->ips[0] !== $previousIp) {
$previousIp = $hop->ips[0];
$finalIdx = $key + 1;
}
}
unset($this->hopsCollection[$finalIdx]);
usort($this->hopsCollection, function ($a, $b) {
return $a->idx - $b->idx;
});
}
}

View file

@ -25,7 +25,7 @@ For this installation we will assume that we are working on AlmaLinux 8 or 9. Wa
Note: These steps also work with AlmaLinux 9, but it will install PHP 8 instead of 7.
1. Install the required network tools: `dnf install mtr traceroute -y`.
2. Install the web server with PHP (by default it will install 7.2): `dnf install httpd mod_ssl php -y`.
2. Install the web server with PHP (by default it will install 7.2 on RHEL 8): `dnf install httpd mod_ssl php php-posix -y`.
3. Enable and start Apache/PHP-FPM: `systemctl enable httpd; systemctl enable php-fpm` and `systemctl start httpd; systemctl start php-fpm`.
4. Let's help MTR to work, execute the following command: `ln -s /usr/sbin/mtr /usr/bin/mtr` and also mtr helper called mtr-packet: `ln -s /usr/sbin/mtr-packet /usr/bin/mtr-packet`.
5. You *must* configure SELinux before this all works, or you can disable SELinux using `setenforce 0` and possibly make it permanent: `nano /etc/selinux/config` change to `SELINUX=disabled`.
@ -33,6 +33,9 @@ Note: These steps also work with AlmaLinux 9, but it will install PHP 8 instead
7. Rename config.dist.php to config.php and adjust the settings.
8. (Optional) You might want to enable SSL using LetsEncrypt, take a look at [acme.sh](https://github.com/acmesh-official/acme.sh).
### Upgrading
Upgrading from a previous version is easy, simply overwrite your current installation with the new files. Then update your config.php accordingly, the script will automatically check for missing variables.
### Customization
If you open up config.dist.php you will see that there are some features that allows you to customize the looking glass, this includes a custom CSS override.
You may also extend the looking glass with a custom block.

View file

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/**
* Hybula Looking Glass
*
@ -11,36 +11,39 @@
* @link https://github.com/hybula/lookingglass
*/
declare(strict_types=1);
require __DIR__.'/config.php';
require __DIR__.'/LookingGlass.php';
require __DIR__.'/config.php';
use Hybula\LookingGlass;
LookingGlass::validateConfig();
LookingGlass::startSession();
if ($_SESSION['TARGET'] && $_SESSION['METHOD'] && isset($_SESSION['BACKEND'])) {
unset($_SESSION['BACKEND']);
switch ($_SESSION['METHOD']) {
case 'ping':
LookingGlass::ping($_SESSION['TARGET']);
if (isset($_SESSION[LookingGlass::SESSION_TARGET_HOST]) &&
isset($_SESSION[LookingGlass::SESSION_TARGET_METHOD]) &&
isset($_SESSION[LookingGlass::SESSION_CALL_BACKEND])
) {
unset($_SESSION[LookingGlass::SESSION_CALL_BACKEND]);
switch ($_SESSION[LookingGlass::SESSION_TARGET_METHOD]) {
case LookingGlass::METHOD_PING:
LookingGlass::ping($_SESSION[LookingGlass::SESSION_TARGET_HOST]);
break;
case 'ping6':
LookingGlass::ping6($_SESSION['TARGET']);
case LookingGlass::METHOD_PING6:
LookingGlass::ping6($_SESSION[LookingGlass::SESSION_TARGET_HOST]);
break;
case 'mtr':
LookingGlass::mtr($_SESSION['TARGET']);
case LookingGlass::METHOD_MTR:
LookingGlass::mtr($_SESSION[LookingGlass::SESSION_TARGET_HOST]);
break;
case 'mtr6':
LookingGlass::mtr6($_SESSION['TARGET']);
case LookingGlass::METHOD_MTR6:
LookingGlass::mtr6($_SESSION[LookingGlass::SESSION_TARGET_HOST]);
break;
case 'traceroute':
LookingGlass::traceroute($_SESSION['TARGET']);
case LookingGlass::METHOD_TRACEROUTE:
LookingGlass::traceroute($_SESSION[LookingGlass::SESSION_TARGET_HOST]);
break;
case 'traceroute6':
LookingGlass::traceroute6($_SESSION['TARGET']);
case LookingGlass::METHOD_TRACEROUTE6:
LookingGlass::traceroute6($_SESSION[LookingGlass::SESSION_TARGET_HOST]);
break;
}
}

70
bootstrap.php Normal file
View file

@ -0,0 +1,70 @@
<?php declare(strict_types=1);
/**
* Hybula Looking Glass
*
* Provides UI and input for the looking glass backend.
*
* @copyright 2022 Hybula B.V.
* @license Mozilla Public License 2.0
* @version 1.1.0
* @since File available since release 1.1.0
* @link https://github.com/hybula/lookingglass
*/
use Hybula\LookingGlass;
if (!file_exists(__DIR__ . '/config.php')) {
die('config.php is not found, but is required for application to work!');
}
require __DIR__ . '/LookingGlass.php';
require __DIR__ . '/config.php';
LookingGlass::validateConfig();
LookingGlass::startSession();
function exitErrorMessage(string $message): void
{
unset($_SESSION[LookingGlass::SESSION_CALL_BACKEND]);
$_SESSION[LookingGlass::SESSION_ERROR_MESSAGE] = $message;
exitNormal();
}
function exitNormal(): void
{
header('Location: /');
exit;
}
$templateData = [
'title' => LG_TITLE,
'custom_css' => LG_CSS_OVERRIDES,
'custom_head' => LG_CUSTOM_HEAD,
'logo_url' => LG_LOGO_URL,
'logo_data' => LG_LOGO,
//
'block_network' => LG_BLOCK_NETWORK,
'block_lookingglas' => LG_BLOCK_LOOKINGGLAS,
'block_speedtest' => LG_BLOCK_SPEEDTEST,
'block_custom' => LG_BLOCK_CUSTOM,
'custom_html' => '',
//
'locations' => LG_LOCATIONS,
'current_location' => LG_LOCATION,
'maps_query' => LG_MAPS_QUERY,
'facility' => LG_FACILITY,
'facility_url' => LG_FACILITY_URL,
'ipv4' => LG_IPV4,
'ipv6' => LG_IPV6,
'methods' => LG_METHODS,
'user_ip' => LookingGlass::detectIpAddress(),
//
'speedtest_iperf' => LG_SPEEDTEST_IPERF,
'speedtest_incoming_label' => LG_SPEEDTEST_LABEL_INCOMING,
'speedtest_incoming_cmd' => LG_SPEEDTEST_CMD_INCOMING,
'speedtest_outgoing_label' => LG_SPEEDTEST_LABEL_OUTGOING,
'speedtest_outgoing_cmd' => LG_SPEEDTEST_CMD_OUTGOING,
'speedtest_files' => LG_SPEEDTEST_FILES,
//
'tos' => LG_TERMS,
'error_message' => false,
];

View file

@ -1,5 +1,5 @@
<?php
declare(strict_types=1);
<?php declare(strict_types=1);
use Hybula\LookingGlass;
// Define the HTML title;
const LG_TITLE = 'Looking Glass';
@ -11,6 +11,8 @@ const LG_LOGO_URL = 'https://github.com/hybula/lookingglass/';
// Define a custom CSS file which can be used to style the LG, set false to disable, else point to the CSS file;
const LG_CSS_OVERRIDES = false;
// Define <head> content, this could be JS, CSS or meta tags;
const LG_CUSTOM_HEAD = false;
// Enable or disable blocks/parts of the LG, set false to hide a part;
const LG_BLOCK_NETWORK = true;
@ -26,6 +28,8 @@ const LG_CUSTOM_PHP = __DIR__.'/custom.post.php';
// Define the location of this network, usually a city and a country;
const LG_LOCATION = 'Amsterdam, Netherlands';
// Define a query location for the link to openstreetmap (eg: Amsterdam, Netherlands will be https://www.openstreetmap.org/search?query=Amsterdam, Netherlands)
const LG_MAPS_QUERY = 'Amsterdam, Netherlands';
// Define the facility where the network is located, usually a data center;
const LG_FACILITY = 'Nikhef';
// Define a direct link to more information about the facility, this should be a link to PeeringDB;
@ -36,7 +40,14 @@ const LG_IPV4 = '127.0.0.1';
const LG_IPV6 = '::1';
// Define the methods that can be used by visitors to test it out;
const LG_METHODS = ['ping', 'ping6', 'mtr', 'mtr6', 'traceroute', 'traceroute6'];
const LG_METHODS = [
LookingGlass::METHOD_PING,
LookingGlass::METHOD_PING6,
LookingGlass::METHOD_MTR,
LookingGlass::METHOD_MTR6,
LookingGlass::METHOD_TRACEROUTE,
LookingGlass::METHOD_TRACEROUTE6,
];
// Define other looking glasses, this is useful if you have multiple networks and looking glasses;
const LG_LOCATIONS = [

View file

@ -1,8 +1,10 @@
FROM php:8.1-fpm-bullseye
RUN apt update && apt install iputils-ping mtr traceroute -y
RUN apt-get update && \
apt-get --no-install-recommends -y install iputils-ping mtr traceroute && \
rm -rf /var/lib/apt/lists/*
WORKDIR /var/www/html
COPY . .
COPY --chown=www-data:www-data . .
COPY docker/php-fpm/src/config.php config.php

View file

@ -0,0 +1,75 @@
<?php declare(strict_types=1);
use Hybula\LookingGlass;
// Define the HTML title;
const LG_TITLE = 'Looking Glass';
// Define a logo, this can be HTML too, see the other example for an image;
const LG_LOGO = '<h2>Company Looking Glass</h2>';
// Define the URL where the logo points to;
const LG_LOGO_URL = 'https://github.com/hybula/lookingglass/';
// Define a custom CSS file which can be used to style the LG, set false to disable, else point to the CSS file;
const LG_CSS_OVERRIDES = false;
// Enable or disable blocks/parts of the LG, set false to hide a part;
const LG_BLOCK_NETWORK = true;
const LG_BLOCK_LOOKINGGLAS = true;
const LG_BLOCK_SPEEDTEST = true;
// This enables the custom block, which you can use to add something custom to the LG;
define('LG_BLOCK_CUSTOM', getenv('ENABLE_CUSTOM_BLOCK') !== false);
// Define a file here which will be used to display the custom block, can be PHP too which outputs HTML;
const LG_CUSTOM_HTML = __DIR__.'/custom.html.php';
// Define a file here which will be loaded on top of the index file, this can be used to do some post logic;
const LG_CUSTOM_PHP = __DIR__.'/custom.post.php';
// Define the location of this network, usually a city and a country;
define('LG_LOCATION', getenv('LOCATION'));
// Define a query location for the link to openstreetmap (eg: Amsterdam, Netherlands will be https://www.openstreetmap.org/search?query=Amsterdam, Netherlands)
define('LG_MAPS_QUERY', getenv('MAPS_QUERY'));
// Define the facility where the network is located, usually a data center;
define('LG_FACILITY', getenv('FACILITY'));
// Define a direct link to more information about the facility, this should be a link to PeeringDB;
define('LG_FACILITY_URL', getenv('FACILITY_URL'));
// Define an IPv4 for testing;
define('LG_IPV4', getenv('IPV4_ADDRESS'));
// Define an IPv6 for testing;
define('LG_IPV6', getenv('IPV6_ADDRESS'));
// Define the methods that can be used by visitors to test it out;
const LG_METHODS = [
LookingGlass::METHOD_PING,
LookingGlass::METHOD_PING6,
LookingGlass::METHOD_MTR,
LookingGlass::METHOD_MTR6,
LookingGlass::METHOD_TRACEROUTE,
LookingGlass::METHOD_TRACEROUTE6,
];
// Define other looking glasses, this is useful if you have multiple networks and looking glasses;
const LG_LOCATIONS = [
'Location A' => 'https://github.com/hybula/lookingglass/',
'Location B' => 'https://github.com/hybula/lookingglass/',
'Location C' => 'https://github.com/hybula/lookingglass/',
];
// Enable the iPerf info inside the speedtest block, set too false to disable;
const LG_SPEEDTEST_IPERF = true;
// Define the label of an incoming iPerf test;
const LG_SPEEDTEST_LABEL_INCOMING = 'iPerf3 Incoming';
// Define the command to use to test incoming speed using iPerf, preferable iPerf3;
const LG_SPEEDTEST_CMD_INCOMING = 'iperf3 -4 -c hostname -p 5201 -P 4';
// Define the label of an outgoing iPerf test;
const LG_SPEEDTEST_LABEL_OUTGOING = 'iPerf3 Outgoing';
// Define the command to use to test outgoing speed using iPerf, preferable iPerf3;
const LG_SPEEDTEST_CMD_OUTGOING = 'iperf3 -4 -c hostname -p 5201 -P 4 -R';
// Define speedtest files with URLs to the actual files;
const LG_SPEEDTEST_FILES = [
'100M' => 'https://github.com/hybula/lookingglass/',
'1G' => 'https://github.com/hybula/lookingglass/',
'10G' => 'https://github.com/hybula/lookingglass/'
];
// Define if you require visitors to agree with the Terms, set false to disable;
define('LG_TERMS', getenv('LG_TERMS') ?: 'https://github.com/hybula/lookingglass/');

299
index.php
View file

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/**
* Hybula Looking Glass
*
@ -11,69 +11,73 @@
* @link https://github.com/hybula/lookingglass
*/
declare(strict_types=1);
require __DIR__.'/config.php';
require __DIR__.'/LookingGlass.php';
require __DIR__.'/bootstrap.php';
use Hybula\LookingGlass;
LookingGlass::validateConfig();
LookingGlass::startSession();
$detectIpAddress = LookingGlass::detectIpAddress();
$errorMessage = null;
if (!empty($_POST)) {
do {
if (!isset($_POST['csrfToken']) || !isset($_SESSION['CSRF']) || ($_POST['csrfToken'] != $_SESSION['CSRF'])) {
$errorMessage = 'Missing or incorrect CSRF token.';
break;
}
if (isset($_POST['submitForm'])) {
if (!in_array($_POST['backendMethod'], LG_METHODS)) {
$errorMessage = 'Unsupported backend method.';
break;
}
$_SESSION['METHOD'] = $_POST['backendMethod'];
$_SESSION['TARGET'] = $_POST['targetHost'];
if (!isset($_POST['checkTerms']) && LG_TERMS) {
$errorMessage = 'You must agree with the Terms of Service.';
break;
}
if (!isset($_POST['csrfToken']) || !isset($_SESSION[LookingGlass::SESSION_CSRF]) || ($_POST['csrfToken'] !== $_SESSION[LookingGlass::SESSION_CSRF])) {
exitErrorMessage('Missing or incorrect CSRF token.');
}
if (in_array($_POST['backendMethod'], ['ping', 'mtr', 'traceroute'])) {
if (!LookingGlass::isValidIpv4($_POST['targetHost'])) {
$targetHost = LookingGlass::isValidHost($_POST['targetHost'], 'ipv4');
if (!$targetHost) {
$errorMessage = 'No valid IPv4 provided.';
break;
}
$_SESSION['TARGET'] = $targetHost;
}
}
if (in_array($_POST['backendMethod'], ['ping6', 'mtr6', 'traceroute6'])) {
if (!LookingGlass::isValidIpv6($_POST['targetHost'])) {
$targetHost = LookingGlass::isValidHost($_POST['targetHost'], 'ipv6');
if (!$targetHost) {
$errorMessage = 'No valid IPv6 provided.';
break;
}
$_SESSION['TARGET'] = $targetHost;
}
}
$_SESSION['TERMS'] = true;
$_SESSION['BACKEND'] = true;
break;
if (!isset($_POST['submitForm']) || !isset($_POST['backendMethod']) || !isset($_POST['targetHost'])) {
exitErrorMessage('Unsupported POST received.');
}
if (!in_array($_POST['backendMethod'], LG_METHODS)) {
exitErrorMessage('Unsupported backend method.');
}
$_SESSION[LookingGlass::SESSION_TARGET_METHOD] = $_POST['backendMethod'];
$_SESSION[LookingGlass::SESSION_TARGET_HOST] = $_POST['targetHost'];
if (!isset($_POST['checkTerms']) && LG_TERMS) {
exitErrorMessage('You must agree with the Terms of Service.');
}
$targetHost = $_POST['targetHost'];
if (in_array($_POST['backendMethod'], ['ping', 'mtr', 'traceroute'])) {
if (!LookingGlass::isValidIpv4($_POST['targetHost']) &&
!$targetHost = LookingGlass::isValidHost($_POST['targetHost'], LookingGlass::IPV4)
) {
exitErrorMessage('No valid IPv4 provided.');
}
$errorMessage = 'Unsupported POST received.';
break;
} while (true);
}
if (in_array($_POST['backendMethod'], ['ping6', 'mtr6', 'traceroute6'])) {
if (!LookingGlass::isValidIpv6($_POST['targetHost']) ||
!$targetHost = LookingGlass::isValidHost($_POST['targetHost'],LookingGlass::IPV6)
) {
exitErrorMessage('No valid IPv6 provided.');
}
}
$_SESSION[LookingGlass::SESSION_TARGET_HOST] = $targetHost;
$_SESSION[LookingGlass::SESSION_TOS_CHECKED] = true;
$_SESSION[LookingGlass::SESSION_CALL_BACKEND] = true;
exitNormal();
}
$_SESSION['CSRF'] = bin2hex(random_bytes(12));
$templateData['session_target'] = $_SESSION[LookingGlass::SESSION_TARGET_HOST] ?? '';
$templateData['session_method'] = $_SESSION[LookingGlass::SESSION_TARGET_METHOD] ?? '';
$templateData['session_call_backend'] = $_SESSION[LookingGlass::SESSION_CALL_BACKEND] ?? false;
$templateData['session_tos_checked'] = isset($_SESSION[LookingGlass::SESSION_TOS_CHECKED]) ? ' checked' : '';
if (isset($_SESSION[LookingGlass::SESSION_ERROR_MESSAGE])) {
$templateData['error_message'] = $_SESSION[LookingGlass::SESSION_ERROR_MESSAGE];
unset($_SESSION[LookingGlass::SESSION_ERROR_MESSAGE]);
}
if (LG_BLOCK_CUSTOM) {
include LG_CUSTOM_PHP;
ob_start();
include LG_CUSTOM_HTML;
$templateData['custom_html'] = ob_get_clean();
}
$templateData['csrfToken'] = $_SESSION[LookingGlass::SESSION_CSRF] = bin2hex(random_bytes(12));
?>
<!doctype html>
<html lang="en">
@ -82,9 +86,10 @@ if (LG_BLOCK_CUSTOM) {
<meta content="width=device-width, initial-scale=1" name="viewport">
<meta content="" name="description">
<meta content="Hybula" name="author">
<title><?php echo LG_TITLE; ?></title>
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" rel="stylesheet">
<?php if (LG_CSS_OVERRIDES) { echo '<link href="'.LG_CSS_OVERRIDES.'" rel="stylesheet">'; } ?>
<title><?php echo $templateData['title'] ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
<?php if ($templateData['custom_css']) { echo '<link href="'.$templateData['custom_css'].'" rel="stylesheet">'; } ?>
<?php if ($templateData['custom_head']) { echo $templateData['custom_head']; } ?>
</head>
<body>
@ -92,23 +97,23 @@ if (LG_BLOCK_CUSTOM) {
<header class="d-flex align-items-center pb-3 mb-5 border-bottom">
<div class="col-8">
<a class="d-flex align-items-center text-dark text-decoration-none" href="<?php echo LG_LOGO_URL; ?>" target="_blank">
<?php echo LG_LOGO; ?>
<a class="d-flex align-items-center text-dark text-decoration-none" href="<?php echo $templateData['logo_url'] ?>" target="_blank">
<?php echo $templateData['logo_data'] ?>
</a>
</div>
<div class="col-4 float-end">
<select class="form-select" onchange="window.location = this.options[this.selectedIndex].value">
<option selected><?php echo LG_LOCATION; ?></option>
<?php foreach (LG_LOCATIONS as $location => $link) { ?>
<option value="<?php echo $link; ?>"><?php echo $location; ?></option>
<?php } ?>
<option selected><?php echo $templateData['current_location'] ?></option>
<?php foreach ($templateData['locations'] as $location => $link): ?>
<option value="<?php echo $link ?>"><?php echo $location ?></option>
<?php endforeach ?>
</select>
</div>
</header>
<main>
<?php if (LG_BLOCK_NETWORK) { ?>
<?php if (LG_BLOCK_NETWORK): ?>
<div class="row mb-5">
<div class="card shadow-lg">
<div class="card-body p-3">
@ -118,23 +123,23 @@ if (LG_BLOCK_CUSTOM) {
<div class="col-md-7">
<label class="mb-2 text-muted">Location</label>
<div class="input-group mb-3">
<input type="text" class="form-control" value="<?php echo LG_LOCATION; ?>" onfocus="this.select()" readonly="">
<a class="btn btn-outline-secondary" href="https://www.openstreetmap.org/search?query=<?php echo urlencode(LG_LOCATION); ?>" target="_blank">Map</a>
<?php if (!empty(LG_LOCATIONS)) { ?>
<input type="text" class="form-control" value="<?php echo $templateData['current_location'] ?>" onfocus="this.select()" readonly="">
<a class="btn btn-outline-secondary" href="https://www.openstreetmap.org/search?query=<?php echo urlencode($templateData['maps_query']); ?>" target="_blank">Map</a>
<?php if (!empty($templateData['locations'])): ?>
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Locations</button>
<ul class="dropdown-menu dropdown-menu-end">
<?php foreach (LG_LOCATIONS as $location => $link) { ?>
<li><a class="dropdown-item" href="<?php echo $link; ?>"><?php echo $location; ?></a></li>
<?php } ?>
<?php foreach ($templateData['locations'] as $location => $link): ?>
<li><a class="dropdown-item" href="<?php echo $link ?>"><?php echo $location ?></a></li>
<?php endforeach ?>
</ul>
<?php } ?>
<?php endif ?>
</div>
</div>
<div class="col-md-5">
<label class="mb-2 text-muted">Facility</label>
<div class="input-group">
<input type="text" class="form-control" value="<?php echo LG_FACILITY; ?>" onfocus="this.select()" readonly="">
<a href="<?php echo LG_FACILITY_URL; ?>" class="btn btn-outline-secondary" target="_blank">PeeringDB</a>
<input type="text" class="form-control" value="<?php echo $templateData['facility'] ?>" onfocus="this.select()" readonly="">
<a href="<?php echo $templateData['facility_url'] ?>" class="btn btn-outline-secondary" target="_blank">PeeringDB</a>
</div>
</div>
</div>
@ -143,22 +148,22 @@ if (LG_BLOCK_CUSTOM) {
<div class="col-md-3">
<label class="mb-2 text-muted">Test IPv4</label>
<div class="input-group">
<input type="text" class="form-control" value="<?php echo LG_IPV4; ?>" onfocus="this.select()" readonly="">
<button class="btn btn-outline-secondary" onclick="copyToClipboard('<?php echo LG_IPV4; ?>', this)">Copy</button>
<input type="text" class="form-control" value="<?php echo $templateData['ipv4'] ?>" onfocus="this.select()" readonly="">
<button class="btn btn-outline-secondary" onclick="copyToClipboard('<?php echo $templateData['ipv4'] ?>', this)">Copy</button>
</div>
</div>
<div class="col-md-5">
<label class="mb-2 text-muted">Test IPv6</label>
<div class="input-group">
<input type="text" class="form-control" value="<?php echo LG_IPV6; ?>" onfocus="this.select()" readonly="">
<button class="btn btn-outline-secondary" onclick="copyToClipboard('<?php echo LG_IPV6; ?>', this)">Copy</button>
<input type="text" class="form-control" value="<?php echo $templateData['ipv6'] ?>" onfocus="this.select()" readonly="">
<button class="btn btn-outline-secondary" onclick="copyToClipboard('<?php echo $templateData['ipv6'] ?>', this)">Copy</button>
</div>
</div>
<div class="col-md-4">
<label class="mb-2 text-muted">Your IP</label>
<div class="input-group">
<input type="text" class="form-control" value="<?php echo $detectIpAddress; ?>" onfocus="this.select()" readonly="">
<button class="btn btn-outline-secondary" onclick="copyToClipboard('<?php echo $detectIpAddress; ?>', this)">Copy</button>
<input type="text" class="form-control" value="<?php echo $templateData['user_ip'] ?>" onfocus="this.select()" readonly="">
<button class="btn btn-outline-secondary" onclick="copyToClipboard('<?php echo $templateData['user_ip'] ?>', this)">Copy</button>
</div>
</div>
</div>
@ -166,48 +171,50 @@ if (LG_BLOCK_CUSTOM) {
</div>
</div>
</div>
<?php } ?>
<?php endif ?>
<?php if (LG_BLOCK_LOOKINGGLAS) { ?>
<?php if (LG_BLOCK_LOOKINGGLAS): ?>
<div class="row pb-5">
<div class="card shadow-lg">
<div class="card-body p-3">
<h1 class="fs-4 card-title mb-4">Looking Glass</h1>
<form method="POST" action="/" autocomplete="off">
<input type="hidden" name="csrfToken" value="<?php echo $_SESSION['CSRF']; ?>">
<input type="hidden" name="csrfToken" value="<?php echo $templateData['csrfToken'] ?>">
<div class="row">
<div class="col-md-7 mb-3">
<div class="input-group">
<span class="input-group-text" id="basic-addon1">Target</span>
<input type="text" class="form-control" placeholder="IP address or host..." name="targetHost" value="<?php if (isset($_SESSION['TARGET'])) { echo $_SESSION['TARGET']; } ?>" required="">
<input type="text" class="form-control" placeholder="IP address or host..." name="targetHost" value="<?php echo $templateData['session_target'] ?>" required="">
</div>
</div>
<div class="col-md-5 mb-3">
<div class="input-group">
<label class="input-group-text">Method</label>
<select class="form-select" name="backendMethod" id="backendMethod">
<?php foreach (LG_METHODS as $method) { ?>
<option value="<?php echo $method; ?>"<?php if (isset($_SESSION['METHOD']) && $_SESSION['METHOD'] == $method) { echo 'selected'; } ?>><?php echo $method; ?></option>
<?php } ?>
<?php foreach ($templateData['methods'] as $method): ?>
<option value="<?php echo $method ?>"<?php if($templateData['session_method'] === $method): ?> selected<?php endif ?>><?php echo $method ?></option>
<?php endforeach ?>
</select>
</div>
</div>
</div>
<div class="d-flex align-items-center">
<?php if (LG_TERMS) { ?>
<?php if ($templateData['tos']): ?>
<div class="form-check">
<input type="checkbox" id="checkTerms" name="checkTerms" class="form-check-input"<?php if (isset($_SESSION['TERMS'])) { echo 'checked'; } ?>>
<label for="checkTerms" class="form-check-label">I agree with the <a href="<?php echo LG_TERMS; ?>" target="_blank">Terms of Use</a></label>
<input type="checkbox" id="checkTerms" name="checkTerms" class="form-check-input"<?php echo $templateData['session_tos_checked'] ?>>
<label for="checkTerms" class="form-check-label">I agree with the <a href="<?php echo $templateData['tos'] ?>" target="_blank">Terms of Use</a></label>
</div>
<?php } ?>
<?php endif ?>
<button type="submit" class="btn btn-primary ms-auto" id="executeButton" name="submitForm">
Execute
</button>
</div>
<?php if (isset($errorMessage)) echo '<div class="alert alert-danger mt-3" role="alert">'.$errorMessage.'</div>'; ?>
<?php if ($templateData['error_message']): ?>
<div class="alert alert-danger mt-3" role="alert"><?php echo $templateData['error_message'] ?></div>
<?php endif ?>
<div class="card card-body bg-light mt-4" style="display: none;" id="outputCard">
<pre id="outputContent" style="overflow: hidden; white-space: pre; word-wrap: normal;"></pre>
@ -217,45 +224,44 @@ if (LG_BLOCK_CUSTOM) {
</div>
</div>
</div>
<?php } ?>
<?php endif ?>
<?php if (LG_BLOCK_SPEEDTEST) { ?>
<?php if (LG_BLOCK_SPEEDTEST): ?>
<div class="row pb-5">
<div class="card shadow-lg">
<div class="card-body p-3">
<h1 class="fs-4 card-title mb-4">Speedtest</h1>
<?php if (LG_SPEEDTEST_IPERF) { ?>
<?php if ($templateData['speedtest_iperf']): ?>
<div class="row mb-3">
<div class="col-md-6">
<label class="mb-2 text-muted"><?php echo LG_SPEEDTEST_LABEL_INCOMING; ?></label>
<p><code><?php echo LG_SPEEDTEST_CMD_INCOMING; ?></code></p>
<button class="btn btn-sm btn-outline-secondary" onclick="copyToClipboard('<?php echo LG_SPEEDTEST_CMD_INCOMING; ?>', this)">Copy</button>
<label class="mb-2 text-muted"><?php echo $templateData['speedtest_incoming_label'] ?></label>
<p><code><?php echo $templateData['speedtest_incoming_cmd']; ?></code></p>
<button class="btn btn-sm btn-outline-secondary" onclick="copyToClipboard('<?php echo $templateData['speedtest_incoming_cmd'] ?>', this)">Copy</button>
</div>
<div class="col-md-6">
<label class="mb-2 text-muted"><?php echo LG_SPEEDTEST_LABEL_OUTGOING; ?></label>
<p><code><?php echo LG_SPEEDTEST_CMD_OUTGOING; ?></code></p>
<button class="btn btn-sm btn-outline-secondary" onclick="copyToClipboard('<?php echo LG_SPEEDTEST_CMD_OUTGOING; ?>', this)">Copy</button>
<label class="mb-2 text-muted"><?php echo $templateData['speedtest_outgoing_label'] ?></label>
<p><code><?php echo $templateData['speedtest_outgoing_cmd'] ?></code></p>
<button class="btn btn-sm btn-outline-secondary" onclick="copyToClipboard('<?php echo $templateData['speedtest_outgoing_cmd'] ?>', this)">Copy</button>
</div>
</div>
<?php } ?>
<?php endif ?>
<div class="row">
<label class="mb-2 text-muted">Test Files</label>
<div class="btn-group input-group mb-3">
<?php foreach (LG_SPEEDTEST_FILES as $file => $link) { ?>
<a href="<?php echo $link; ?>" class="btn btn-outline-secondary"><?php echo $file; ?></a>
<?php } ?>
<?php foreach ($templateData['speedtest_files'] as $file => $link): ?>
<a href="<?php echo $link ?>" class="btn btn-outline-secondary"><?php echo $file ?></a>
<?php endforeach ?>
</div>
</div>
</div>
</div>
</div>
<?php } ?>
<?php if (LG_BLOCK_CUSTOM) { include LG_CUSTOM_HTML; } ?>
<?php endif ?>
<?php echo $templateData['custom_html'] ?>
</main>
<footer class="pt-3 mt-5 my-5 text-muted border-top">
@ -264,42 +270,73 @@ if (LG_BLOCK_CUSTOM) {
</footer>
</div>
<?php if ($templateData['session_call_backend']): ?>
<script type="text/javascript">
<?php if (isset($_SESSION['BACKEND'])) { echo 'callBackend();'; } ?>
function callBackend() {
const executeButton = document.getElementById('executeButton');
executeButton.innerText = 'Executing...';
executeButton.disabled = true;
document.getElementById('outputCard').style.display = 'inherit';
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
document.getElementById('outputContent').innerHTML = this.responseText.replace(/<br \/> +/g, '<br />');
if (this.readyState === XMLHttpRequest.DONE) {
executeButton.innerText = 'Execute';
executeButton.disabled = false;
console.log('Backend ready!');
}
};
xhr.open('GET', 'backend.php', true);
xhr.send();
(function () {
const outputContent = document.getElementById('outputContent')
const executeButton = document.getElementById('executeButton')
const outputCard = document.getElementById('outputCard')
executeButton.innerText = 'Executing...'
executeButton.disabled = true
outputCard.style.display = 'inherit'
fetch('/backend.php')
.then(async (response) => {
// response.body is a ReadableStream
const reader = response.body.getReader()
const decoder = new TextDecoder()
for await (const chunk of readChunks(reader)) {
const text = decoder.decode(chunk)
<?php if(in_array($_SESSION[LookingGlass::SESSION_TARGET_METHOD], [LookingGlass::METHOD_MTR, LookingGlass::METHOD_MTR6])): ?>
let splittedText = text.split('---')
if (!splittedText[1]) {
continue
}
outputContent.innerHTML = splittedText[1].trim()
<?php else: ?>
outputContent.innerHTML = outputContent.innerHTML + text.trim().replace(/<br \/> +/g, '<br />')
<?php endif ?>
}
})
.finally(() => {
executeButton.innerText = 'Execute'
executeButton.disabled = false
console.log('Backend ready!')
})
})()
// readChunks() reads from the provided reader and yields the results into an async iterable
function readChunks(reader) {
return {
async* [Symbol.asyncIterator]() {
let readResult = await reader.read()
while (!readResult.done) {
yield readResult.value
readResult = await reader.read()
}
},
}
}
</script>
<?php endif ?>
<script type="text/javascript">
async function copyToClipboard(text, button) {
button.innerHTML = 'Copied!';
const textAreaObject = document.createElement('textarea');
textAreaObject.value = text;
document.body.appendChild(textAreaObject);
textAreaObject.select();
document.execCommand('copy');
document.body.removeChild(textAreaObject);
await new Promise(r => setTimeout(r, 2000));
button.innerHTML = 'Copy';
if (!navigator || !navigator.clipboard || !navigator.clipboard.writeText) {
return Promise.reject('The Clipboard API is not available.')
}
button.innerHTML = 'Copied!'
await navigator.clipboard.writeText(text)
await new Promise(r => setTimeout(r, 2000))
button.innerHTML = 'Copy'
}
</script>
<script crossorigin="anonymous" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
</body>
</html>