Release 5.4 (#645)

* Initial implementation of ipinfo.io offline database

* Removed unnecessary code

* add: download ipinfo db during docker build

* fixed workflow

* rename warning in workflow

* commit to trigger workflow

* Refactor getIP

* Improved UI

* Updated docker version with 5.4 changes

* Updated README.md

* Added fallback in getIP in case the offline db is missing

* Fixed typos

* just md linting

* Removed vscode stuff

* Implemented fallback in getIP for PHP<8 (returns only the IP)

* Updated doc.md

* Fixed comments in telemetry_settings.php

* New quick start video

* Corrected image name in doc_docker.md

* Replaced speedtest with just test in stats.php

* docker documentation update

* typo

---------

Co-authored-by: Federico Dossena <info@fdossena.com>
Co-authored-by: Stefan Stidl <stefan.stidl@ffg.at>
This commit is contained in:
sstidl 2024-08-04 09:06:27 +02:00 committed by GitHub
parent b419726fef
commit 420be5e72f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 803 additions and 1020 deletions

View File

@ -37,6 +37,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
# Fetch the ipinfo database file using curl if API key is defined
- name: Fetch DB from ipinfo.io
# IPINFO_APIKEY is set in https://github.com/librespeed/speedtest/settings/secrets/actions
run: |
if [ -z "${{ secrets.IPINFO_APIKEY }}" ]; then
echo "Warning: IPINFO_APIKEY is not defined."
else
curl -L https://ipinfo.io/data/free/country_asn.mmdb?token=${{ secrets.IPINFO_APIKEY }} -o backend/country_asn.mmdb
fi
# Set up BuildKit Docker container builder to be able to build
# multi-platform images and export cache
# https://github.com/docker/setup-buildx-action

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
results/idObfuscation_salt.php
backend/getIP_serverLocation.php
db-dir/
.vscode/

View File

@ -1,5 +0,0 @@
{
"recommendations": [
"github.vscode-github-actions"
]
}

View File

@ -7,13 +7,16 @@ No Flash, No Java, No Websocket, No Bullshit.
This is a very lightweight speed test implemented in Javascript, using XMLHttpRequest and Web Workers.
## Try it
[Take a speed test](https://librespeed.org)
## Compatibility
All modern browsers are supported: IE11, latest Edge, latest Chrome, latest Firefox, latest Safari.
Works with mobile versions too.
## Features
* Download
* Upload
* Ping
@ -25,53 +28,62 @@ Works with mobile versions too.
![Screenrecording of a running Speedtest](https://speedtest.fdossena.com/mpot_v6.gif)
## Server requirements
* A reasonably fast web server with Apache 2 (nginx, IIS also supported)
* PHP 5.4 or newer (other backends also available)
* MySQL database to store test results (optional, Microsoft SQL Server, PostgreSQL and SQLite also supported)
* A fast! internet connection
## Installation
Assuming you have PHP installed, the installation steps are quite simple.
I set this up on a QNAP.
For this example, I am using a folder called **speedtest** in my web share area.
1. Choose one of the example-xxx.html files in `examples` folder as your index.html if the default index.html does not fit.
2. Add: speedtest.js, speedtest_worker.js, and favicon.ico to your speedtest folder.
3. Download all of the backend folder into speedtest/backend.
4. Download all of the results folder into speedtest/results.
5. Be sure your permissions allow execute (755).
6. Visit YOURSITE/speedtest/index.html and voila!
Assuming you have PHP and a web server installed, the installation steps are quite simple.
1. Download the source code and extract it
1. Copy the following files to your web server's shared folder (ie. /var/www/html/speedtest for Apache): index.html, speedtest.js, speedtest_worker.js, favicon.ico and the backend folder
1. Optionally, copy the results folder too, and set up the database using the config file in it.
1. Be sure your permissions allow execute (755).
1. Visit YOURSITE/speedtest/index.html and voila!
### Installation Video
There is a more in-depth installation video here:
* [Quick start installation guide for Ubuntu Server 19.04](https://fdossena.com/?p=speedtest/quickstart_v5_ubuntu.frag)
This video shows the installation process of a standalone LibreSpeed server: [Quick start installation guide for Debian 12](https://fdossena.com/?p=speedtest/quickstart_deb12.frag)
More videos will be added later.
## Android app
A template to build an Android client for your LibreSpeed installation is available [here](https://github.com/librespeed/speedtest-android).
## CLI client
A command line client is available [here](https://github.com/librespeed/speedtest-cli).
## Docker
A docker image is available on [GitHub](https://github.com/librespeed/speedtest/pkgs/container/speedtest), check our [docker documentation](doc_docker.md) for more info about it.
The image is built every week to include an updated version of the ipinfo-DB used for ISP detection. Also this ensures, that the latest security patches in PHP are installed. Therefore we recommend to use the `latest` image.
## Go backend
A Go implementation is available in the [`speedtest-go`](https://github.com/librespeed/speedtest-go) repo, maintained by [Maddie Zhan](https://github.com/maddie).
## Rust backend
A Rust implementation is available in the [`speedtest-rust`](https://github.com/librespeed/speedtest-rust) repo, maintained by [Sudo Dios](https://github.com/sudodios).
## Node.js backend
A partial Node.js implementation is available in the `node` branch, developed by [dunklesToast](https://github.com/dunklesToast). It's not recommended to use at the moment.
## Donate
[![Donate with Liberapay](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/fdossena/donate)
[Donate with PayPal](https://www.paypal.me/sineisochronic)
## License
Copyright (C) 2016-2022 Federico Dossena
Copyright (C) 2016-2024 Federico Dossena
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by

BIN
backend/country_asn.mmdb Normal file

Binary file not shown.

BIN
backend/geoip2.phar Normal file

Binary file not shown.

View File

@ -2,7 +2,7 @@
/*
* This script detects the client's IP address and fetches ISP info from ipinfo.io/
* Output from this script is a JSON string composed of 2 objects: a string called processedString which contains the combined IP, ISP, Country and distance as it can be presented to the user; and an object called rawIspInfo which contains the raw data from ipinfo.io (will be empty if isp detection is disabled).
* Output from this script is a JSON string composed of 2 objects: a string called processedString which contains the combined IP, ISP, Country and distance as it can be presented to the user; and an object called rawIspInfo which contains the raw data from ipinfo.io (or an empty string if isp detection is disabled or if it failed).
* Client side, the output of this script can be treated as JSON or as regular text. If the output is regular text, it will be shown to the user as is.
*/
@ -10,399 +10,194 @@ error_reporting(0);
define('API_KEY_FILE', 'getIP_ipInfo_apikey.php');
define('SERVER_LOCATION_CACHE_FILE', 'getIP_serverLocation.php');
define('OFFLINE_IPINFO_DB_FILE', 'country_asn.mmdb');
require_once 'getIP_util.php';
/**
* @param string $ip
*
* @return string|null
*/
function getLocalOrPrivateIpInfo($ip)
{
function getLocalOrPrivateIpInfo($ip){
// ::1/128 is the only localhost ipv6 address. there are no others, no need to strpos this
if ('::1' === $ip) {
return 'localhost IPv6 access';
}
// simplified IPv6 link-local address (should match fe80::/10)
if (stripos($ip, 'fe80:') === 0) {
return 'link-local IPv6 access';
}
// fc00::/7 Unique Local IPv6 Unicast Addresses
if (preg_match('/^(fc|fd)([0-9a-f]{0,4}:){1,7}[0-9a-f]{1,4}$/i', $ip) === 1) {
return 'ULA IPv6 access';
}
// anything within the 127/8 range is localhost ipv4, the ip must start with 127.0
if (strpos($ip, '127.') === 0) {
return 'localhost IPv4 access';
}
// 10/8 private IPv4
if (strpos($ip, '10.') === 0) {
return 'private IPv4 access';
}
// 172.16/12 private IPv4
if (preg_match('/^172\.(1[6-9]|2\d|3[01])\./', $ip) === 1) {
return 'private IPv4 access';
}
// 192.168/16 private IPv4
if (strpos($ip, '192.168.') === 0) {
return 'private IPv4 access';
}
// IPv4 link-local
if (strpos($ip, '169.254.') === 0) {
return 'link-local IPv4 access';
}
return null;
}
/**
* @return string
*/
function getIpInfoTokenString()
{
if (
!file_exists(API_KEY_FILE)
|| !is_readable(API_KEY_FILE)
) {
return '';
function getIspInfo_ipinfoApi($ip){
if (!file_exists(API_KEY_FILE) || !is_readable(API_KEY_FILE)){
return null;
}
require API_KEY_FILE;
if (empty($IPINFO_APIKEY)) {
return '';
if(empty($IPINFO_APIKEY)){
return null;
}
return '?token=' . $IPINFO_APIKEY;
}
/**
* @param string $ip
*
* @return array|null
*/
function getIspInfo($ip)
{
$json = file_get_contents('https://ipinfo.io/' . $ip . '/json' . getIpInfoTokenString());
$json = file_get_contents('https://ipinfo.io/' . $ip . '/json?token=' . $IPINFO_APIKEY);
if (!is_string($json)) {
return null;
}
$data = json_decode($json, true);
if (!is_array($data)) {
return null;
}
return $data;
}
/**
* @param array|null $rawIspInfo
*
* @return string
*/
function getIsp($rawIspInfo)
{
if (is_array($rawIspInfo)) {
/* variant with no token
has json like:
{
"ip": "xxx.xxx.xxx.xxx",
"hostname": "example.com",
"city": "Vienna",
"region": "Vienna",
"country": "AT",
"loc": "48.2085,16.3721",
"org": "ASxxxx T-Mobile Austria GmbH",
"postal": "nnnn",
"timezone": "Europe/Vienna",
"readme": "https://ipinfo.io/missingauth"
$isp=null;
//ISP name, if present, is either in org or asn.name
if (array_key_exists('org', $data) && is_string($data['org']) && !empty($data['org'])) {
// Remove AS##### from ISP name, if present
$isp = preg_replace('/AS\\d+\\s/', '', $data['org']);
} elseif (array_key_exists('asn', $data) && is_array($data['asn']) && !empty($data['asn']) && array_key_exists('name', $data['asn']) && is_string($data['asn']['name'])) {
$isp = $data['asn']['name'];
} else{
return null;
}
$country=null;
if(array_key_exists('country',$data) && is_string($data['country'])){
$country = $data['country'];
}
//If requested by the client (and we have the required information), calculate the distance
$distance=null;
if(isset($_GET['distance']) && ($_GET['distance']==='mi' || $_GET['distance']==='km') && array_key_exists('loc', $data) && is_string($data['loc'])){
$unit = $_GET['distance'];
$clientLoc = $data['loc'];
$serverLoc = null;
if (file_exists(SERVER_LOCATION_CACHE_FILE) && is_readable(SERVER_LOCATION_CACHE_FILE)) {
require SERVER_LOCATION_CACHE_FILE;
}
*/
if (
array_key_exists('org', $rawIspInfo)
&& is_string($rawIspInfo['org'])
&& !empty($rawIspInfo['org'])
) {
// Remove AS##### from ISP name, if present
return preg_replace('/AS\\d+\\s/', '', $rawIspInfo['org']);
}
/*
variant with valid token has json:
{
"ip": "xxx.xxx.xxx.xxx",
"hostname": "example.com",
"city": "Vienna",
"region": "Vienna",
"country": "AT",
"loc": "48.2085,16.3721",
"postal": "1010",
"timezone": "Europe/Vienna",
"asn": {
"asn": "ASxxxx",
"name": "T-Mobile Austria GmbH",
"domain": "t-mobile.at",
"route": "xxx.xxx.xxx.xxx/xx",
"type": "isp"
},
"company": {
"name": "XX",
"domain": "example.com",
"type": "isp"
},
"privacy": {
"vpn": true,
"proxy": false,
"tor": false,
"relay": false,
"hosting": false,
"service": ""
},
"abuse": {
"address": "...",
"country": "AT",
"email": "abuse@example.com",
"name": "XXX",
"network": "xxx.xxx.xxx.xxx-xxx.xxx.xxx.xxx",
"phone": ""
},
"domains": {
"total": 0,
"domains": [
]
if (!is_string($serverLoc) || empty($serverLoc)) {
$json = file_get_contents('https://ipinfo.io/json?token=' . $IPINFO_APIKEY);
if (!is_string($json)) {
return null;
}
$sdata = json_decode($json, true);
if (!is_array($sdata) || !array_key_exists('loc', $sdata) || !is_string($sdata['loc']) || empty($sdata['loc'])) {
return null;
}
*/
if (
array_key_exists('asn', $rawIspInfo)
&& is_array($rawIspInfo['asn'])
&& !empty($rawIspInfo['asn'])
&& array_key_exists('name', $rawIspInfo['asn'])
&& is_string($rawIspInfo['asn']['name'])
) {
// Remove AS##### from ISP name, if present
return $rawIspInfo['asn']['name'];
$serverLoc = $sdata['loc'];
file_put_contents(SERVER_LOCATION_CACHE_FILE, "<?php\n\n\$serverLoc = '" . addslashes($serverLoc) . "';\n");
}
list($clientLatitude, $clientLongitude) = explode(',', $clientLoc);
list($serverLatitude, $serverLongitude) = explode(',', $serverLoc);
//distance calculation adapted from http://www.codexworld.com
$rad = M_PI / 180;
$dist = acos(sin($clientLatitude * $rad) * sin($serverLatitude * $rad) + cos($clientLatitude * $rad) * cos($serverLatitude * $rad) * cos(($clientLongitude - $serverLongitude) * $rad)) / $rad * 60 * 1.853;
if ($unit === 'mi') {
$dist /= 1.609344;
$dist = round($dist, -1);
if ($dist < 15) {
$dist = '<15';
}
$distance = $dist . ' mi';
}elseif ($unit === 'km') {
$dist = round($dist, -1);
if ($dist < 20) {
$dist = '<20';
}
$distance = $dist . ' km';
}
}
return 'Unknown ISP';
}
/**
* @return string|null
*/
function getServerLocation()
{
$serverLoc = null;
if (
file_exists(SERVER_LOCATION_CACHE_FILE)
&& is_readable(SERVER_LOCATION_CACHE_FILE)
) {
require SERVER_LOCATION_CACHE_FILE;
$processedString=$ip.' - '.$isp;
if(is_string($country)){
$processedString.=', '.$country;
}
if (is_string($serverLoc) && !empty($serverLoc)) {
return $serverLoc;
if(is_string($distance)){
$processedString.=' ('.$distance.')';
}
$json = file_get_contents('https://ipinfo.io/json' . getIpInfoTokenString());
if (!is_string($json)) {
return null;
}
$details = json_decode($json, true);
if (
!is_array($details)
|| !array_key_exists('loc', $details)
|| !is_string($details['loc'])
|| empty($details['loc'])
) {
return null;
}
$serverLoc = $details['loc'];
$cacheData = "<?php\n\n\$serverLoc = '" . addslashes($serverLoc) . "';\n";
file_put_contents(SERVER_LOCATION_CACHE_FILE, $cacheData);
return $serverLoc;
}
/**
* Optimized algorithm from http://www.codexworld.com
*
* @param float $latitudeFrom
* @param float $longitudeFrom
* @param float $latitudeTo
* @param float $longitudeTo
*
* @return float [km]
*/
function distance(
$latitudeFrom,
$longitudeFrom,
$latitudeTo,
$longitudeTo
) {
$rad = M_PI / 180;
$theta = $longitudeFrom - $longitudeTo;
$dist = sin($latitudeFrom * $rad)
* sin($latitudeTo * $rad)
+ cos($latitudeFrom * $rad)
* cos($latitudeTo * $rad)
* cos($theta * $rad);
return acos($dist) / $rad * 60 * 1.853;
}
/**
* @param array|null $rawIspInfo
*
* @return string|null
*/
function getDistance($rawIspInfo)
{
if (
!is_array($rawIspInfo)
|| !array_key_exists('loc', $rawIspInfo)
|| !isset($_GET['distance'])
|| !in_array($_GET['distance'], ['mi', 'km'], true)
) {
return null;
}
$unit = $_GET['distance'];
$clientLocation = $rawIspInfo['loc'];
$serverLocation = getServerLocation();
if (!is_string($serverLocation)) {
return null;
}
return calculateDistance(
$serverLocation,
$clientLocation,
$unit
);
}
/**
* @param string $clientLocation
* @param string $serverLocation
* @param string $unit
*
* @return string
*/
function calculateDistance($clientLocation, $serverLocation, $unit)
{
list($clientLatitude, $clientLongitude) = explode(',', $clientLocation);
list($serverLatitude, $serverLongitude) = explode(',', $serverLocation);
$dist = distance(
$clientLatitude,
$clientLongitude,
$serverLatitude,
$serverLongitude
);
if ('mi' === $unit) {
$dist /= 1.609344;
$dist = round($dist, -1);
if ($dist < 15) {
$dist = '<15';
}
return $dist . ' mi';
}
if ('km' === $unit) {
$dist = round($dist, -1);
if ($dist < 20) {
$dist = '<20';
}
return $dist . ' km';
}
return null;
}
/**
* @return void
*/
function sendHeaders()
{
header('Content-Type: application/json; charset=utf-8');
if (isset($_GET['cors'])) {
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST');
}
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0, s-maxage=0');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
}
/**
* @param string $ip
* @param string|null $ipInfo
* @param string|null $distance
* @param array|null $rawIspInfo
*
* @return void
*/
function sendResponse(
$ip,
$ipInfo = null,
$distance = null,
$rawIspInfo = null
) {
$processedString = $ip;
if (is_string($ipInfo)) {
$processedString .= ' - ' . $ipInfo;
}
if (
is_array($rawIspInfo)
&& array_key_exists('country', $rawIspInfo)
) {
$processedString .= ', ' . $rawIspInfo['country'];
}
if (is_string($distance)) {
$processedString .= ' (' . $distance . ')';
}
sendHeaders();
echo json_encode([
return json_encode([
'processedString' => $processedString,
'rawIspInfo' => $rawIspInfo ?: '',
'rawIspInfo' => $data ?: '',
]);
}
if (PHP_MAJOR_VERSION >= 8){
require_once("geoip2.phar");
}
function getIspInfo_ipinfoOfflineDb($ip){
if (!file_exists(OFFLINE_IPINFO_DB_FILE) || !is_readable(OFFLINE_IPINFO_DB_FILE)){
return null;
}
$reader = new MaxMind\Db\Reader(OFFLINE_IPINFO_DB_FILE);
$data = $reader->get($ip);
if(!is_array($data)){
return null;
}
$processedString = $ip.' - ' . $data['as_name'] . ', ' . $data['country_name'];
return json_encode([
'processedString' => $processedString,
'rawIspInfo' => $data ?: '',
]);
}
function formatResponse_simple($ip,$ispName=null){
$processedString=$ip;
if(is_string($ispName)){
$processedString.=' - '.$ispName;
}
return json_encode([
'processedString' => $processedString,
'rawIspInfo' => '',
]);
}
header('Content-Type: application/json; charset=utf-8');
if (isset($_GET['cors'])) {
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST');
}
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0, s-maxage=0');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
$ip = getClientIp();
$localIpInfo = getLocalOrPrivateIpInfo($ip);
// local ip, no need to fetch further information
if (is_string($localIpInfo)) {
sendResponse($ip, $localIpInfo);
exit;
//if the user requested the ISP info, we first try to fetch it using ipinfo.io (if there is no api key set it fails without sending data, it can also fail because of rate limiting or invalid responses), then we try with the offline db, if that also fails (or if ISP info was not requested) we just respond with the IP address
if(isset($_GET['isp'])){
$localIpInfo = getLocalOrPrivateIpInfo($ip);
//local ip, no need to fetch further information
if (is_string($localIpInfo)) {
echo formatResponse_simple($ip,$localIpInfo);
}else{
//ipinfo API and offline db require PHP 8 or newer
if (PHP_MAJOR_VERSION >= 8){
$r=getIspInfo_ipinfoApi($ip);
if(!is_null($r)){
echo $r;
}else{
$r=getIspInfo_ipinfoOfflineDb($ip);
if(!is_null($r)){
echo $r;
}else{
echo formatResponse_simple($ip);
}
}
}else{
echo formatResponse_simple($ip);
}
}
}else{
echo formatResponse_simple($ip);
}
if (!isset($_GET['isp'])) {
sendResponse($ip);
exit;
}
$rawIspInfo = getIspInfo($ip);
$isp = getIsp($rawIspInfo);
$distance = getDistance($rawIspInfo);
sendResponse($ip, $isp, $distance, $rawIspInfo);

1
backend/getIP_util.php Normal file → Executable file
View File

@ -17,4 +17,3 @@ function getClientIp() {
return preg_replace('/^::ffff:/', '', $ip);
}

479
doc.md

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,23 @@
# Using the docker image
A docker version of LibreSpeed is available here: [GitHub Packages](https://github.com/librespeed/speedtest/pkgs/container/speedtest)
## Downloading docker image
To download LibreSpeed from the docker repo, use this command:
## Quickstart
```
docker pull ghcr.io/librespeed/speedtest
If you just want to try it, the fastest way is:
```shell
docker run -p 80:80 -d --name speedtest --rm ghcr.io/librespeed/speedtest
```
You will now have a new docker image called `librespeed/speedtest`.
Then go with your browser to port 80 of your server and try it out. If port 80 is already in use, adjust the first number in 80:80 above.
Default is to run in standalone mode.
## Docker Compose
To start the container using [docker compose](https://docs.docker.com/compose/) the following configuration can be used:
In production environments we would recommend using docker-compose.
To start the container using [docker compose](https://docs.docker.com/compose/) the following `docker-compose.yml` configuration can be used:
```yml
version: '3.7'
@ -29,6 +35,7 @@ services:
#PASSWORD:
#EMAIL:
#DISABLE_IPINFO: "false"
#IPINFO_APIKEY: "your api key"
#DISTANCE: "km"
#WEBPORT: 80
ports:
@ -38,21 +45,31 @@ services:
Please adjust the environment variables according to the intended operating mode.
## Standalone mode
If you want to install LibreSpeed on a single server, you need to configure it in standalone mode. To do this, set the `MODE` environment variable to `standalone`.
The test can be accessed on port 80.
Here's a list of additional environment variables available in this mode:
* __`TITLE`__: Title of your speed test. Default value: `LibreSpeed`
* __`TELEMETRY`__: Whether to enable telemetry or not. If enabled, you maybe want your data to be persisted. See below. Default value: `false`
* __`ENABLE_ID_OBFUSCATION`__: When set to true with telemetry enabled, test IDs are obfuscated, to avoid exposing the database internal sequential IDs. Default value: `false`
* __`REDACT_IP_ADDRESSES`__: When set to true with telemetry enabled, IP addresses and hostnames are redacted from the collected telemetry, for better privacy. Default value: `false`
* __`DB_TYPE`__: When set to one of the supported DB-Backends it will use this instead of the default sqlite database backend. TELEMETRY has to be set to `true`. Also you have to create the database as described in [doc.md](doc.md#creating-the-database). Supported backend types are:
* sqlite - no additional settings required
* mysql, postgresql - set additional env-variables:
* DB_HOSTNAME - Name or IP of the DB server
* DB_PORT (mysql only) - Port where DB is running
* DB_NAME - Name of the telemetry db
* DB_USERNAME, DB_PASSWORD - credentials of the user with read and update permissions to the db
* mssql - not supported in docker image yet (feel free to open a PR with that, has to be done in `entrypoint.sh`)
* __`PASSWORD`__: Password to access the stats page. If not set, stats page will not allow accesses.
* __`EMAIL`__: Email address for GDPR requests. Must be specified when telemetry is enabled.
* __`IPINFO_APIKEY`__: API key for ipinfo.io. Optional, but required if you expect to serve a large number of tests
* __`DISABLE_IPINFO`__: If set to true, ISP info and distance will not be fetched from ipinfo.io. Default: value: `false`
* __`DISTANCE`__: When `DISABLE_IPINFO` is set to false, this specifies how the distance from the server is measured. Can be either `km` for kilometers, `mi` for miles, or an empty string to disable distance measurement. Default value: `km`
* __`WEBPORT`__: Allows choosing a custom port for the included web server. Default value: `80`. Note that you will have to expose it through docker with the -p argument
* __`DISABLE_IPINFO`__: If set to `true`, ISP info and distance will not be fetched from either [ipinfo.io](https://ipinfo.io) or the offline database. Default: value: `false`
* __`IPINFO_APIKEY`__: API key for [ipinfo.io](https://ipinfo.io). Optional, but required if you want to use the full [ipinfo.io](https://ipinfo.io) APIs (required for distance measurement)
* __`DISTANCE`__: When `DISABLE_IPINFO` is set to false, this specifies how the distance from the server is measured. Can be either `km` for kilometers, `mi` for miles, or an empty string to disable distance measurement. Requires an [ipinfo.io](https://ipinfo.io) API key. Default value: `km`
* __`WEBPORT`__: Allows choosing a custom port for the included web server. Default value: `80`. Note that you will have to expose it through docker with the -p argument. This is not the port where the service is exposed outside docker!
If telemetry is enabled, a stats page will be available at `http://your.server/results/stats.php`, but a password must be specified.
@ -62,42 +79,44 @@ Default DB driver is sqlite. The DB file is written to `/database/db.sql`.
So if you want your data to be persisted over image updates, you have to mount a volume with `-v $PWD/db-dir:/database`.
#### Example Standalone Mode with telemetry
###### Example
This command starts LibreSpeed in standalone mode, with the default settings, on port 80:
This command starts LibreSpeed in standalone mode, with persisted telemetry, ID obfuscation and a stats password, on port 86:
```
docker run -e MODE=standalone -p 80:80 -it ghcr.io/librespeed/speedtest
```
This command starts LibreSpeed in standalone mode, with telemetry, ID obfuscation and a stats password, on port 86:
```
```shell
docker run -e MODE=standalone -e TELEMETRY=true -e ENABLE_ID_OBFUSCATION=true -e PASSWORD="yourPasswordHere" -e WEBPORT=86 -p 86:86 -v $PWD/db-dir/:/database -it ghcr.io/librespeed/speedtest
```
## Multiple Points of Test
For multiple servers, you need to set up 1+ LibreSpeed backends, and 1 LibreSpeed frontend.
### Backend mode
In backend mode, LibreSpeed provides only a test point with no UI. To do this, set the `MODE` environment variable to `backend`.
The following backend files can be accessed on port 80: `garbage.php`, `empty.php`, `getIP.php`
Here's a list of additional environment variables available in this mode:
* __`IPINFO_APIKEY`__: API key for ipinfo.io. Optional, but required if you expect to serve a large number of tests
###### Example:
* __`IPINFO_APIKEY`__: API key for [ipinfo.io](https://ipinfo.io). Optional, but required if you want to use the full [ipinfo.io](https://ipinfo.io) APIs (required for distance measurement). If no API key is provided, the offline database will be used instead.
#### Example Backend mode
This command starts LibreSpeed in backend mode, with the default settings, on port 80:
```
```shell
docker run -e MODE=backend -p 80:80 -it ghcr.io/librespeed/speedtest
```
### Frontend mode
In frontend mode, LibreSpeed serves clients the Web UI and a list of servers. To do this:
* Set the `MODE` environment variable to `frontend`
* Create a servers.json file with your test points. The syntax is the following:
```
```jsonc
[
{
"name": "Friendly name for Server 1",
@ -115,34 +134,30 @@ In frontend mode, LibreSpeed serves clients the Web UI and a list of servers. To
"pingURL" :"empty.php",
"getIpURL" :"getIP.php"
},
...more servers...
//...more servers...
]
```
Note: if a server only supports HTTP or HTTPS, specify the protocol in the server field. If it supports both, just use `//`.
* Mount this file to `/servers.json` in the container (example at the end of this file)
The test can be accessed on port 80.
Here's a list of additional environment variables available in this mode:
* __`TITLE`__: Title of your speedtest. Default value: `LibreSpeed`
* __`TELEMETRY`__: Whether to enable telemetry or not. Default value: `false`
* __`ENABLE_ID_OBFUSCATION`__: When set to true with telemetry enabled, test IDs are obfuscated, to avoid exposing the database internal sequential IDs. Default value: `false`
* __`REDACT_IP_ADDRESSES`__: When set to true with telemetry enabled, IP addresses and hostnames are redacted from the collected telemetry, for better privacy. Default value: `false`
* __`PASSWORD`__: Password to access the stats page. If not set, stats page will not allow accesses.
* __`EMAIL`__: Email address for GDPR requests. Must be specified when telemetry is enabled.
* __`DISABLE_IPINFO`__: If set to true, ISP info and distance will not be fetched from ipinfo.io. Default: value: `false`
* __`DISTANCE`__: When `DISABLE_IPINFO` is set to false, this specifies how the distance from the server is measured. Can be either `km` for kilometers, `mi` for miles, or an empty string to disable distance measurement. Default value: `km`
* __`WEBPORT`__: Allows choosing a custom port for the included web server. Default value: `80`
The list of environment variables available in this mode is the same as [above in standalone mode](#standalone-mode).
###### Example
This command starts LibreSpeed in frontend mode, with a given `servers.json` file, and with telemetry, ID obfuscation, and a stats password:
```
docker run -e MODE=frontend -e TELEMETRY=true -e ENABLE_ID_OBFUSCATION=true -e PASSWORD="yourPasswordHere" -v $(pwd)/servers.json:/servers.json -p 80:80 -it ghcr.io/librespeed/speedtest
#### Example Frontend mode
This command starts LibreSpeed in frontend mode, with a given `servers.json` file, and with telemetry, ID obfuscation, and a stats password and a persistant sqlite database for results:
```shell
docker run -e MODE=frontend -e TELEMETRY=true -e ENABLE_ID_OBFUSCATION=true -e PASSWORD="yourPasswordHere" -v $PWD/servers.json:/servers.json -v $PWD/db-dir/:/database -p 80:80 -it ghcr.io/librespeed/speedtest
```
### Dual mode
In dual mode, LibreSpeed operates as a standalone server that can also connect to other test points.
To do this:
* Set the `MODE` environment variable to `dual`
* Follow the `servers.json` instructions for the frontend mode
* The first server entry should be the local server, using the server endpoint address that a client can access.
* The first server entry should be the local server, using the server endpoint address that a client can access.

View File

@ -27,11 +27,9 @@ if [ "$MODE" == "backend" ]; then
fi
fi
# Set up index.php for frontend-only or standalone modes
if [[ "$MODE" == "frontend" || "$MODE" == "dual" ]]; then
cp /speedtest/frontend.php /var/www/html/index.php
elif [ "$MODE" == "standalone" ]; then
cp /speedtest/standalone.php /var/www/html/index.php
# Set up unified index.php
if [ "$MODE" != "backend" ]; then
cp /speedtest/ui.php /var/www/html/index.php
fi
# Apply Telemetry settings when running in standalone or frontend mode and telemetry is enabled

View File

@ -1,367 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no" />
<link rel="apple-touch-icon" href="favicon.ico">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta charset="UTF-8" />
<link rel="shortcut icon" href="favicon.ico">
<script type="text/javascript" src="speedtest.js"></script>
<script type="text/javascript">
function I(i){return document.getElementById(i);}
//INITIALIZE SPEED TEST
var s=new Speedtest(); //create speed test object
<?php if(getenv("TELEMETRY")=="true"){ ?>
s.setParameter("telemetry_level","basic");
<?php } ?>
<?php if(getenv("DISABLE_IPINFO")=="true"){ ?>
s.setParameter("getIp_ispInfo",false);
<?php } ?>
<?php if(getenv("DISTANCE")){ ?>
s.setParameter("getIp_ispInfo_distance","<?=getenv("DISTANCE") ?>");
<?php } ?>
var meterBk=/Trident.*rv:(\d+\.\d+)/i.test(navigator.userAgent)?"#EAEAEA":"#80808040";
var dlColor="#6060AA",
ulColor="#616161";
var progColor=meterBk;
//CODE FOR GAUGES
function drawMeter(c,amount,bk,fg,progress,prog){
var ctx=c.getContext("2d");
var dp=window.devicePixelRatio||1;
var cw=c.clientWidth*dp, ch=c.clientHeight*dp;
var sizScale=ch*0.0055;
if(c.width==cw&&c.height==ch){
ctx.clearRect(0,0,cw,ch);
}else{
c.width=cw;
c.height=ch;
}
ctx.beginPath();
ctx.strokeStyle=bk;
ctx.lineWidth=12*sizScale;
ctx.arc(c.width/2,c.height-58*sizScale,c.height/1.8-ctx.lineWidth,-Math.PI*1.1,Math.PI*0.1);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle=fg;
ctx.lineWidth=12*sizScale;
ctx.arc(c.width/2,c.height-58*sizScale,c.height/1.8-ctx.lineWidth,-Math.PI*1.1,amount*Math.PI*1.2-Math.PI*1.1);
ctx.stroke();
if(typeof progress !== "undefined"){
ctx.fillStyle=prog;
ctx.fillRect(c.width*0.3,c.height-16*sizScale,c.width*0.4*progress,4*sizScale);
}
}
function mbpsToAmount(s){
return 1-(1/(Math.pow(1.3,Math.sqrt(s))));
}
function format(d){
d=Number(d);
if(d<10) return d.toFixed(2);
if(d<100) return d.toFixed(1);
return d.toFixed(0);
}
//UI CODE
var uiData=null;
function startStop(){
if(s.getState()==3){
//speed test is running, abort
s.abort();
data=null;
I("startStopBtn").className="";
initUI();
}else{
//test is not running, begin
I("startStopBtn").className="running";
I("shareArea").style.display="none";
s.onupdate=function(data){
uiData=data;
};
s.onend=function(aborted){
I("startStopBtn").className="";
updateUI(true);
if(!aborted){
//if testId is present, show sharing panel, otherwise do nothing
try{
var testId=uiData.testId;
if(testId!=null){
var shareURL=window.location.href.substring(0,window.location.href.lastIndexOf("/"))+"/results/?id="+testId;
I("resultsImg").src=shareURL;
I("resultsURL").value=shareURL;
I("testId").innerHTML=testId;
I("shareArea").style.display="";
}
}catch(e){}
}
};
s.start();
}
}
//this function reads the data sent back by the test and updates the UI
function updateUI(forced){
if(!forced&&s.getState()!=3) return;
if(uiData==null) return;
var status=uiData.testState;
I("ip").textContent=uiData.clientIp;
I("dlText").textContent=(status==1&&uiData.dlStatus==0)?"...":format(uiData.dlStatus);
drawMeter(I("dlMeter"),mbpsToAmount(Number(uiData.dlStatus*(status==1?oscillate():1))),meterBk,dlColor,Number(uiData.dlProgress),progColor);
I("ulText").textContent=(status==3&&uiData.ulStatus==0)?"...":format(uiData.ulStatus);
drawMeter(I("ulMeter"),mbpsToAmount(Number(uiData.ulStatus*(status==3?oscillate():1))),meterBk,ulColor,Number(uiData.ulProgress),progColor);
I("pingText").textContent=format(uiData.pingStatus);
I("jitText").textContent=format(uiData.jitterStatus);
}
function oscillate(){
return 1+0.02*Math.sin(Date.now()/100);
}
//update the UI every frame
window.requestAnimationFrame=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.msRequestAnimationFrame||(function(callback,element){setTimeout(callback,1000/60);});
function frame(){
requestAnimationFrame(frame);
updateUI();
}
frame(); //start frame loop
//function to (re)initialize UI
function initUI(){
drawMeter(I("dlMeter"),0,meterBk,dlColor,0);
drawMeter(I("ulMeter"),0,meterBk,ulColor,0);
I("dlText").textContent="";
I("ulText").textContent="";
I("pingText").textContent="";
I("jitText").textContent="";
I("ip").textContent="";
}
</script>
<style type="text/css">
html,body{
border:none; padding:0; margin:0;
background:#FFFFFF;
color:#202020;
}
body{
text-align:center;
font-family:"Roboto",sans-serif;
}
h1{
color:#404040;
}
#startStopBtn{
display:inline-block;
margin:0 auto;
color:#6060AA;
background-color:rgba(0,0,0,0);
border:0.15em solid #6060FF;
border-radius:0.3em;
transition:all 0.3s;
box-sizing:border-box;
width:8em; height:3em;
line-height:2.7em;
cursor:pointer;
box-shadow: 0 0 0 rgba(0,0,0,0.1), inset 0 0 0 rgba(0,0,0,0.1);
}
#startStopBtn:hover{
box-shadow: 0 0 2em rgba(0,0,0,0.1), inset 0 0 1em rgba(0,0,0,0.1);
}
#startStopBtn.running{
background-color:#FF3030;
border-color:#FF6060;
color:#FFFFFF;
}
#startStopBtn:before{
content:"Start";
}
#startStopBtn.running:before{
content:"Abort";
}
#test{
margin-top:2em;
margin-bottom:12em;
}
div.testArea{
display:inline-block;
width:16em;
height:12.5em;
position:relative;
box-sizing:border-box;
}
div.testArea2{
display:inline-block;
width:14em;
height:7em;
position:relative;
box-sizing:border-box;
text-align:center;
}
div.testArea div.testName{
position:absolute;
top:0.1em; left:0;
width:100%;
font-size:1.4em;
z-index:9;
}
div.testArea2 div.testName{
display:block;
text-align:center;
font-size:1.4em;
}
div.testArea div.meterText{
position:absolute;
bottom:1.55em; left:0;
width:100%;
font-size:2.5em;
z-index:9;
}
div.testArea2 div.meterText{
display:inline-block;
font-size:2.5em;
}
div.meterText:empty:before{
content:"0.00";
}
div.testArea div.unit{
position:absolute;
bottom:2em; left:0;
width:100%;
z-index:9;
}
div.testArea2 div.unit{
display:inline-block;
}
div.testArea canvas{
position:absolute;
top:0; left:0; width:100%; height:100%;
z-index:1;
}
div.testGroup{
display:block;
margin: 0 auto;
}
#shareArea{
width:95%;
max-width:40em;
margin:0 auto;
margin-top:2em;
}
#shareArea > *{
display:block;
width:100%;
height:auto;
margin: 0.25em 0;
}
#privacyPolicy{
position:fixed;
top:2em;
bottom:2em;
left:2em;
right:2em;
overflow-y:auto;
width:auto;
height:auto;
box-shadow:0 0 3em 1em #000000;
z-index:999999;
text-align:left;
background-color:#FFFFFF;
padding:1em;
}
a.privacy{
text-align:center;
font-size:0.8em;
color:#808080;
display:block;
}
@media all and (max-width:40em){
body{
font-size:0.8em;
}
}
</style>
<title><?= getenv('TITLE') ?: 'LibreSpeed Example' ?></title>
</head>
<body>
<h1><?= getenv('TITLE') ?: 'LibreSpeed Example' ?></h1>
<div id="testWrapper">
<div id="startStopBtn" onclick="startStop()"></div><br/>
<?php if(getenv("TELEMETRY")=="true"){ ?>
<a class="privacy" href="#" onclick="I('privacyPolicy').style.display=''">Privacy</a>
<?php } ?>
<div id="test">
<div class="testGroup">
<div class="testArea2">
<div class="testName">Ping</div>
<div id="pingText" class="meterText" style="color:#AA6060"></div>
<div class="unit">ms</div>
</div>
<div class="testArea2">
<div class="testName">Jitter</div>
<div id="jitText" class="meterText" style="color:#AA6060"></div>
<div class="unit">ms</div>
</div>
</div>
<div class="testGroup">
<div class="testArea">
<div class="testName">Download</div>
<canvas id="dlMeter" class="meter"></canvas>
<div id="dlText" class="meterText"></div>
<div class="unit">Mbit/s</div>
</div>
<div class="testArea">
<div class="testName">Upload</div>
<canvas id="ulMeter" class="meter"></canvas>
<div id="ulText" class="meterText"></div>
<div class="unit">Mbit/s</div>
</div>
</div>
<div id="ipArea">
<span id="ip"></span>
</div>
<div id="shareArea" style="display:none">
<h3>Share results</h3>
<p>Test ID: <span id="testId"></span></p>
<input type="text" value="" id="resultsURL" readonly="readonly" onclick="this.select();this.focus();this.select();document.execCommand('copy');alert('Link copied')"/>
<img src="" id="resultsImg" />
</div>
</div>
<a href="https://github.com/librespeed/speedtest">Source code</a>
</div>
<div id="privacyPolicy" style="display:none">
<h2>Privacy Policy</h2>
<p>This HTML5 speed test server is configured with telemetry enabled.</p>
<h4>What data we collect</h4>
<p>
At the end of the test, the following data is collected and stored:
<ul>
<li>Test ID</li>
<li>Time of testing</li>
<li>Test results (download and upload speed, ping and jitter)</li>
<li>IP address</li>
<li>ISP information</li>
<li>Approximate location (inferred from IP address, not GPS)</li>
<li>User agent and browser locale</li>
<li>Test log (contains no personal information)</li>
</ul>
</p>
<h4>How we use the data</h4>
<p>
Data collected through this service is used to:
<ul>
<li>Allow sharing of test results (sharable image for forums, etc.)</li>
<li>To improve the service offered to you (for instance, to detect problems on our side)</li>
</ul>
No personal information is disclosed to third parties.
</p>
<h4>Your consent</h4>
<p>
By starting the test, you consent to the terms of this privacy policy.
</p>
<h4>Data removal</h4>
<p>
If you want to have your information deleted, you need to provide either the ID of the test or your IP address. This is the only way to identify your data, without this information we won't be able to comply with your request.<br/><br/>
Contact this email address for all deletion requests: <a href="mailto:<?=getenv("EMAIL") ?>"><?=getenv("EMAIL") ?></a>.
</p>
<br/><br/>
<a class="privacy" href="#" onclick="I('privacyPolicy').style.display='none'">Close</a><br/>
</div>
<script type="text/javascript">setTimeout(function(){initUI()},100);</script>
</body>
</html>

View File

@ -3,15 +3,19 @@
<head>
<link rel="shortcut icon" href="favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no" />
<link rel="apple-touch-icon" href="favicon.ico">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta charset="UTF-8" />
<script type="text/javascript" src="speedtest.js"></script>
<script type="text/javascript">
function I(i){return document.getElementById(i);}
//LIST OF TEST SERVERS. See documentation for details if needed
<?php
$mode=getenv("MODE");
if($mode=="standalone" || $mode=="dual"){ ?>
var SPEEDTEST_SERVERS=[];
<?php } else { ?>
var SPEEDTEST_SERVERS= <?= file_get_contents('/servers.json') ?: '[]' ?>;
<?php } ?>
//INITIALIZE SPEED TEST
var s=new Speedtest(); //create speed test object
@ -27,45 +31,53 @@ s.setParameter("getIp_ispInfo_distance","<?=getenv("DISTANCE") ?>");
//SERVER AUTO SELECTION
function initServers(){
var noServersAvailable=function(){
I("message").innerHTML="No servers available";
}
var runServerSelect=function(){
s.selectServer(function(server){
if(server!=null){ //at least 1 server is available
I("loading").className="hidden"; //hide loading message
//populate server list for manual selection
for(var i=0;i<SPEEDTEST_SERVERS.length;i++){
if(SPEEDTEST_SERVERS[i].pingT==-1) continue;
var option=document.createElement("option");
option.value=i;
option.textContent=SPEEDTEST_SERVERS[i].name;
if(SPEEDTEST_SERVERS[i]===server) option.selected=true;
I("server").appendChild(option);
}
//show test UI
I("testWrapper").className="visible";
initUI();
}else{ //no servers are available, the test cannot proceed
noServersAvailable();
}
});
}
if(typeof SPEEDTEST_SERVERS === "string"){
//need to fetch list of servers from specified URL
s.loadServerList(SPEEDTEST_SERVERS,function(servers){
if(servers==null){ //failed to load server list
noServersAvailable();
}else{ //server list loaded
SPEEDTEST_SERVERS=servers;
runServerSelect();
}
});
}else{
//hardcoded server list
s.addTestPoints(SPEEDTEST_SERVERS);
runServerSelect();
}
if(SPEEDTEST_SERVERS.length==0){ //standalone installation
//just make the UI visible
I("loading").className="hidden";
I("serverArea").style.display="none";
I("testWrapper").className="visible";
initUI();
}else{ //multiple servers
var noServersAvailable=function(){
I("message").innerHTML="No servers available";
}
var runServerSelect=function(){
s.selectServer(function(server){
if(server!=null){ //at least 1 server is available
I("loading").className="hidden"; //hide loading message
//populate server list for manual selection
for(var i=0;i<SPEEDTEST_SERVERS.length;i++){
if(SPEEDTEST_SERVERS[i].pingT==-1) continue;
var option=document.createElement("option");
option.value=i;
option.textContent=SPEEDTEST_SERVERS[i].name;
if(SPEEDTEST_SERVERS[i]===server) option.selected=true;
I("server").appendChild(option);
}
//show test UI
I("testWrapper").className="visible";
initUI();
}else{ //no servers are available, the test cannot proceed
noServersAvailable();
}
});
}
if(typeof SPEEDTEST_SERVERS === "string"){
//need to fetch list of servers from specified URL
s.loadServerList(SPEEDTEST_SERVERS,function(servers){
if(servers==null){ //failed to load server list
noServersAvailable();
}else{ //server list loaded
SPEEDTEST_SERVERS=servers;
runServerSelect();
}
});
}else{
//hardcoded server list
s.addTestPoints(SPEEDTEST_SERVERS);
runServerSelect();
}
}
}
var meterBk=/Trident.*rv:(\d+\.\d+)/i.test(navigator.userAgent)?"#EAEAEA":"#80808040";
@ -344,8 +356,15 @@ function initUI(){
text-align:center;
font-size:0.8em;
color:#808080;
display:block;
padding: 0 3em;
}
div.closePrivacyPolicy {
width: 100%;
text-align: center;
}
div.closePrivacyPolicy a.privacy {
padding: 1em 3em;
}
@media all and (max-width:40em){
body{
font-size:0.8em;
@ -377,18 +396,36 @@ function initUI(){
opacity:0;
}
}
@media all and (prefers-color-scheme: dark){
html,body,#loading{
background:#202020;
color:#F4F4F4;
}
h1{
color:#E0E0E0;
}
a{
color:#9090FF;
}
#privacyPolicy{
background:#000000;
}
#resultsImg{
filter: invert(1);
}
}
</style>
<title><?= getenv('TITLE') ?: 'LibreSpeed Example' ?></title>
<title><?= getenv('TITLE') ?: 'LibreSpeed' ?></title>
</head>
<body onload="initServers()">
<h1><?= getenv('TITLE') ?: 'LibreSpeed Example' ?></h1>
<h1><?= getenv('TITLE') ?: 'LibreSpeed' ?></h1>
<div id="loading" class="visible">
<p id="message"><span class="loadCircle"></span>Selecting a server...</p>
</div>
<div id="testWrapper" class="hidden">
<div id="startStopBtn" onclick="startStop()"></div>
<div id="startStopBtn" onclick="startStop()"></div><br/>
<?php if(getenv("TELEMETRY")=="true"){ ?>
<a class="privacy" href="#" onclick="I('privacyPolicy').style.display=''">Privacy</a>
<a class="privacy" href="#" onclick="I('privacyPolicy').style.display=''">Privacy</a>
<?php } ?>
<div id="serverArea">
Server: <select id="server" onchange="s.setSelectedServer(SPEEDTEST_SERVERS[this.value])"></select>
@ -468,7 +505,10 @@ function initUI(){
Contact this email address for all deletion requests: <a href="mailto:<?=getenv("EMAIL") ?>"><?=getenv("EMAIL") ?></a>.
</p>
<br/><br/>
<a class="privacy" href="#" onclick="I('privacyPolicy').style.display='none'">Close</a><br/>
<div class="closePrivacyPolicy">
<a class="privacy" href="#" onclick="I('privacyPolicy').style.display='none'">Close</a>
</div>
<br/>
</div>
</body>
</html>

View File

@ -1,15 +1,88 @@
<!DOCTYPE html>
<html>
<head>
<link rel="shortcut icon" href="favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no" />
<meta charset="UTF-8" />
<link rel="shortcut icon" href="favicon.ico">
<script type="text/javascript" src="speedtest.js"></script>
<script type="text/javascript">
function I(i){return document.getElementById(i);}
//LIST OF TEST SERVERS. Leave empty if you're doing a standalone installation. See documentation for details
var SPEEDTEST_SERVERS=[
/*{ //this server doesn't actually exist, remove it
name:"Example Server 1", //user friendly name for the server
server:"//test1.mydomain.com/", //URL to the server. // at the beginning will be replaced with http:// or https:// automatically
dlURL:"backend/garbage.php", //path to download test on this server (garbage.php or replacement)
ulURL:"backend/empty.php", //path to upload test on this server (empty.php or replacement)
pingURL:"backend/empty.php", //path to ping/jitter test on this server (empty.php or replacement)
getIpURL:"backend/getIP.php" //path to getIP on this server (getIP.php or replacement)
},
{ //this server doesn't actually exist, remove it
name:"Example Server 2", //user friendly name for the server
server:"//test2.example.com/", //URL to the server. // at the beginning will be replaced with http:// or https:// automatically
dlURL:"garbage.php", //path to download test on this server (garbage.php or replacement)
ulURL:"empty.php", //path to upload test on this server (empty.php or replacement)
pingURL:"empty.php", //path to ping/jitter test on this server (empty.php or replacement)
getIpURL:"getIP.php" //path to getIP on this server (getIP.php or replacement)
}*/
//add other servers here, comma separated
];
//INITIALIZE SPEEDTEST
var s=new Speedtest(); //create speedtest object
s.setParameter("telemetry_level","basic"); //enable telemetry
var s=new Speedtest(); //create speed test object
s.setParameter("telemetry_level","basic"); //enable basic telemetry (for results sharing)
//SERVER AUTO SELECTION
function initServers(){
if(SPEEDTEST_SERVERS.length==0){ //standalone installation
//just make the UI visible
I("loading").className="hidden";
I("serverArea").style.display="none";
I("testWrapper").className="visible";
initUI();
}else{ //multiple servers
var noServersAvailable=function(){
I("message").innerHTML="No servers available";
}
var runServerSelect=function(){
s.selectServer(function(server){
if(server!=null){ //at least 1 server is available
I("loading").className="hidden"; //hide loading message
//populate server list for manual selection
for(var i=0;i<SPEEDTEST_SERVERS.length;i++){
if(SPEEDTEST_SERVERS[i].pingT==-1) continue;
var option=document.createElement("option");
option.value=i;
option.textContent=SPEEDTEST_SERVERS[i].name;
if(SPEEDTEST_SERVERS[i]===server) option.selected=true;
I("server").appendChild(option);
}
//show test UI
I("testWrapper").className="visible";
initUI();
}else{ //no servers are available, the test cannot proceed
noServersAvailable();
}
});
}
if(typeof SPEEDTEST_SERVERS === "string"){
//need to fetch list of servers from specified URL
s.loadServerList(SPEEDTEST_SERVERS,function(servers){
if(servers==null){ //failed to load server list
noServersAvailable();
}else{ //server list loaded
SPEEDTEST_SERVERS=servers;
runServerSelect();
}
});
}else{
//hardcoded server list
s.addTestPoints(SPEEDTEST_SERVERS);
runServerSelect();
}
}
}
var meterBk=/Trident.*rv:(\d+\.\d+)/i.test(navigator.userAgent)?"#EAEAEA":"#80808040";
var dlColor="#6060AA",
@ -57,20 +130,23 @@ function format(d){
var uiData=null;
function startStop(){
if(s.getState()==3){
//speedtest is running, abort
//speed test is running, abort
s.abort();
data=null;
I("startStopBtn").className="";
I("server").disabled=false;
initUI();
}else{
//test is not running, begin
I("startStopBtn").className="running";
I("shareArea").style.display="none";
I("server").disabled=true;
s.onupdate=function(data){
uiData=data;
};
s.onend=function(aborted){
I("startStopBtn").className="";
I("server").disabled=false;
updateUI(true);
if(!aborted){
//if testId is present, show sharing panel, otherwise do nothing
@ -136,6 +212,25 @@ function initUI(){
h1{
color:#404040;
}
#loading{
background-color:#FFFFFF;
color:#404040;
text-align:center;
}
span.loadCircle{
display:inline-block;
width:2em;
height:2em;
vertical-align:middle;
background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAP1BMVEUAAAB2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZyFzwnAAAAFHRSTlMAEvRFvX406baecwbf0casimhSHyiwmqgAAADpSURBVHja7dbJbQMxAENRahnN5lkc//5rDRAkDeRgHszXgACJoKiIiIiIiIiIiIiIiIiIiIj4HHspsrpAVhdVVguzrA4OWc10WcEqpwKbnBo0OU1Q5NSpsoJFTgOecrrdEag85DRgktNqfoEdTjnd7hrEHMEJvmRUYJbTYk5Agy6nau6Abp5Cm7mDBtRdPi9gyKdU7w4p1fsLvyqs8hl4z9/w3n/Hmr9WoQ65lAU4d7lMYOz//QboRR5jBZibLMZdAR6O/Vfa1PlxNr3XdS3HzK/HVPRu/KnLs8iAOh993VpRRERERMT/fAN60wwWaVyWwAAAAABJRU5ErkJggg==');
background-size:2em 2em;
margin-right:0.5em;
animation: spin 0.6s linear infinite;
}
@keyframes spin{
0%{transform:rotate(0deg);}
100%{transform:rotate(359deg);}
}
#startStopBtn{
display:inline-block;
margin:0 auto;
@ -164,6 +259,13 @@ function initUI(){
#startStopBtn.running:before{
content:"Abort";
}
#serverArea{
margin-top:1em;
}
#server{
font-size:1em;
padding:0.2em;
}
#test{
margin-top:2em;
margin-bottom:12em;
@ -259,7 +361,7 @@ function initUI(){
font-size:0.8em;
color:#808080;
padding: 0 3em;
}
}
div.closePrivacyPolicy {
width: 100%;
text-align: center;
@ -272,17 +374,67 @@ function initUI(){
font-size:0.8em;
}
}
div.visible{
animation: fadeIn 0.4s;
display:block;
}
div.hidden{
animation: fadeOut 0.4s;
display:none;
}
@keyframes fadeIn{
0%{
opacity:0;
}
100%{
opacity:1;
}
}
@keyframes fadeOut{
0%{
display:block;
opacity:1;
}
100%{
display:block;
opacity:0;
}
}
@media all and (prefers-color-scheme: dark){
html,body,#loading{
background:#202020;
color:#F4F4F4;
}
h1{
color:#E0E0E0;
}
a{
color:#9090FF;
}
#privacyPolicy{
background:#000000;
}
#resultsImg{
filter: invert(1);
}
}
</style>
<title>LibreSpeed Example</title>
<title>LibreSpeed</title>
</head>
<body>
<h1>LibreSpeed Example</h1>
<div id="testWrapper">
<body onload="initServers()">
<h1>LibreSpeed</h1>
<div id="loading" class="visible">
<p id="message"><span class="loadCircle"></span>Selecting a server...</p>
</div>
<div id="testWrapper" class="hidden">
<div id="startStopBtn" onclick="startStop()"></div><br/>
<a class="privacy" href="#" onclick="I('privacyPolicy').style.display=''">Privacy</a>
<div id="serverArea">
Server: <select id="server" onchange="s.setSelectedServer(SPEEDTEST_SERVERS[this.value])"></select>
</div>
<div id="test">
<div class="testGroup">
<div class="testArea2">
<div class="testArea2">
<div class="testName">Ping</div>
<div id="pingText" class="meterText" style="color:#AA6060"></div>
<div class="unit">ms</div>
@ -360,6 +512,5 @@ function initUI(){
</div>
<br/>
</div>
<script type="text/javascript">setTimeout(function(){initUI()},100);</script>
</body>
</html>

0
results/sanitycheck.php Normal file → Executable file
View File

View File

@ -86,18 +86,18 @@ header('Pragma: no-cache');
$speedtest = getSpeedtestUserById($_GET['id']);
$speedtests = [];
if (false === $speedtest) {
echo '<div>There was an error trying to fetch the speedtest result for ID "'.htmlspecialchars($_GET['id'], ENT_HTML5, 'UTF-8').'".</div>';
echo '<div>There was an error trying to fetch the test result for ID "'.htmlspecialchars($_GET['id'], ENT_HTML5, 'UTF-8').'".</div>';
} elseif (null === $speedtest) {
echo '<div>Could not find a speedtest result for ID "'.htmlspecialchars($_GET['id'], ENT_HTML5, 'UTF-8').'".</div>';
echo '<div>Could not find a test result for ID "'.htmlspecialchars($_GET['id'], ENT_HTML5, 'UTF-8').'".</div>';
} else {
$speedtests = [$speedtest];
}
} else {
$speedtests = getLatestSpeedtestUsers();
if (false === $speedtests) {
echo '<div>There was an error trying to fetch latest speedtest results.</div>';
echo '<div>There was an error trying to fetch latest test results.</div>';
} elseif (empty($speedtests)) {
echo '<div>Could not find any speedtest results in database.</div>';
echo '<div>Could not find any test results in database.</div>';
}
}
foreach ($speedtests as $speedtest) {

View File

@ -106,20 +106,21 @@ function getPdo($returnErrorMessage = false)
$pdo = new PDO('sqlite:'.$Sqlite_db_file, null, null, $pdoOptions);
# TODO: Why create table only in sqlite mode?
$pdo->exec('
CREATE TABLE IF NOT EXISTS `speedtest_users` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`ispinfo` text,
`extra` text,
`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`ip` text NOT NULL,
`ua` text NOT NULL,
`lang` text NOT NULL,
`dl` text,
`ul` text,
`ping` text,
`jitter` text,
`log` longtext
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`ispinfo` text,
`extra` text,
`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`ip` text NOT NULL,
`ua` text NOT NULL,
`lang` text NOT NULL,
`dl` text,
`ul` text,
`ping` text,
`jitter` text,
`log` longtext
);
');

0
results/telemetry_mssql.sql Normal file → Executable file
View File

View File

@ -15,11 +15,11 @@ $Sqlite_db_file = '../../speedtest_telemetry.sql';
// mssql settings
$MsSql_server = 'DB_HOSTNAME';
$MsSql_databasename = 'DB_NAME';
$MsSql_WindowsAuthentication = true; #true or false
$MsSql_username = 'USERNAME'; #not used if MsSql_WindowsAuthentication is true
$MsSql_password = 'PASSWORD'; #not used if MsSql_WindowsAuthentication is true
$MsSql_TrustServerCertificate = true; #true, false or comment out for driver default
#Download driver from https://docs.microsoft.com/en-us/sql/connect/php/download-drivers-php-sql-server?view=sql-server-ver16
$MsSql_WindowsAuthentication = true; //true or false
$MsSql_username = 'USERNAME'; //not used if MsSql_WindowsAuthentication is true
$MsSql_password = 'PASSWORD'; //not used if MsSql_WindowsAuthentication is true
$MsSql_TrustServerCertificate = true; //true, false or comment out for driver default
//Download driver from https://docs.microsoft.com/en-us/sql/connect/php/download-drivers-php-sql-server?view=sql-server-ver16
// Mysql settings
$MySql_username = 'USERNAME';

View File

@ -49,7 +49,7 @@ function Speedtest() {
this._settings = {}; //settings for the speed test worker
this._state = 0; //0=adding settings, 1=adding servers, 2=server selection done, 3=test running, 4=done
console.log(
"LibreSpeed by Federico Dossena v5.3.1 - https://github.com/librespeed/speedtest"
"LibreSpeed by Federico Dossena v5.4 - https://github.com/librespeed/speedtest"
);
}