Added test result sharing; Added stats page; Improved project structure; Minor bug fixes

This commit is contained in:
adolfintel 2018-08-06 18:15:32 +02:00
parent cfbbfa3e58
commit 5446e98933
15 changed files with 671 additions and 35 deletions

View file

@ -17,6 +17,7 @@ Only modern browsers are supported (IE11, latest Edge, latest Chrome, latest Fir
* Jitter
* IP Address
* Telemetry (optional)
* Results sharing (optional)
## Requirements
- A reasonably fast web server with PHP (see doc.md for details and use without PHP)

49
doc.md
View file

@ -1,7 +1,7 @@
# HTML5 Speedtest
> by Federico Dossena
> Version 4.5.5, April 25, 2018
> Version 4.6, August 7, 2018
> [https://github.com/adolfintel/speedtest/](https://github.com/adolfintel/speedtest/)
@ -19,8 +19,8 @@ First of all, the requirements to run this test:
* Apple Safari 7.1+
* Opera 18+
* Client side, the test can use up to 500 megabytes of RAM
* Server side, you'll need a fast connection (at least 100 Mbps recommended), and the web server must accept large POST requests (up to 20 megabytes).
Apache2 and PHP are recommended, but not mandatory.
* Server side, you'll need a fast connection (at least 100 Mbps recommended), and the web server must accept large POST requests (up to 20 megabytes).
The recommended setup is: GNU/Linux, Apache2, PHP, MySQL database (if you want to use telemetry).
If this looks good, let's proceed and see how to use the test.
@ -37,7 +37,7 @@ To install the test on your server, upload the following files:
* `empty.php`
* one of the examples
Later we'll see how to use the test without PHP, and how to configure the telemetry if you want to use it.
Later we'll see how to use the test without PHP, and how to configure the telemetry and result sharing if you want to use that.
__Important:__ keep all the files together; all paths are relative to the js file
@ -51,7 +51,10 @@ Start by copying one of the included examples. Here's a description for each of
* `example-customSettings2.html`: A modified version of `example-pretty.html` showing how to make a custom test with only download and upload
* `example-gauges.html`: The most sophisticated example, with the same functions as `example-pretty.html` but also gauges and progress indicators for each test. This is the nicest example included, and also a good starting point, but drawing the gauges may slow down the test on slow devices like a Raspberry Pi
* `example-chart.html`: The old example5.html, showing how to use the test with the Chart.js library
These 2 examples require some additional server configuration, discussed in the Telemetry section:
* `example-telemetry.html`: A modified version of `example-pretty.html` with basic telemetry turned on. See the section on Telemetry for details
* `example-telemetry-resultsSharing.html`: A modified version of `example-telemetry.html` with results sharing. This is the most complete and most complex example, showing off all of the speedtest features
### Customizing your example
The included examples are good starting places if you want to have a simple speedtest on your site.
@ -114,6 +117,7 @@ w.onmessage = function (event) {
var dlProgress = data[6]
var ulProgress = data[7]
var pingProgress = data[8]
var testId = data[9]
if (testState >= 4) {
clearInterval(timer) // test is finished or aborted
}
@ -157,6 +161,7 @@ format:
* __dlProgress__: the progress of the download test as a number between 0 and 1
* __ulProgress__: the progress of the upload test as a number between 0 and 1
* __pingProgress__: the progress of the ping+jitter test as a number between 0 and 1
* __testId__: when telemetry is active, this is the ID of the test as an integer. This string is empty until the test is finished (testState 4). This ID is used for results sharing
Note: clientIp appears before jitterStatus. This is not a mistake, it's to keep the js file compatible with older pages from before the jitter test was introduced.
@ -303,11 +308,12 @@ It is important here to turn off compression, and generate incompressible data.
A symlink to `/dev/urandom` is also ok.
#### Replacement for `empty.php`
Your replacement must simply respond with a HTTP code 200 and send nothing else. You may want to send additional headers to disable caching. The test assumes that Connection:keep-alive is sent by the server.
Your replacement must simply respond with a HTTP code 200 and send nothing else. You may want to send additional headers to disable caching. The test assumes that Connection:keep-alive is sent by the server.
An empty file can be used for this.
#### Replacement for `getIP.php`
Your replacement must simply respond with the client's IP as plaintext. Nothing fancy.
If you want, you can also accept the `isp=true` parameter and also include the ISP info.
Your replacement can simply respond with the client's IP as plaintext or do something more fancy.
If you want, you can also accept the `isp=true` parameter and also include the ISP info. In this case, return a JSON string containing a string called `processedString` with the text that you want to be displayed in the IP address field, and an object called `rawIspInfo` containing whatever you want (will be included in telemetry if enabled).
#### JS
You need to start the test with your replacements like this:
@ -318,18 +324,18 @@ w.postMessage('start {"url_dl": "newGarbageURL", "url_ul": "newEmptyURL", "url_p
## Telemetry
Telemetry currently requires PHP and either MySQL, PostgreSQL or SQLite. Alternatively, it is possible to save to a CSV file.
To set up the telemetry, we need to do 4 things:
* copy `telemetry.php` and `telemetry_settings.php`
* copy the `telemetry` folder
* edit `telemetry_settings.php` to add your database or CSV settings
* create the database
* enable telemetry
### Creating the database
This step is only for MySQL and PostgreSQL.
Log into your database using phpMyAdmin or a similar software and import the appropriate sql file into an empty database. For MySQL databases use `telemetry_mysql.sql` and for PostgreSQL databases use `telemetry_postgesql.sql`.
Log into your database using phpMyAdmin or a similar software and import the appropriate sql file into an empty database. For MySQL databases use `telemetry_mysql.sql` and for PostgreSQL databases use `telemetry_postgesql.sql`. They're inside the `telemetry` folder. You can delete the files afterwards.
If you see a table called `speedtest_users`, empty, you did it right.
### Configuring `telemetry.php`
Open telemetry_settings.php with notepad or a similar text editor.
Open `telemetry_settings.php` with notepad or a similar text editor.
Set your preferred database, ``$db_type="mysql";``, ``$db_type="sqlite";``, ``$db_type="postgresql";`` or ``$db_type="csv";``
If you choose to use Sqlite3, you must set the path to your database file:
```php
@ -340,7 +346,7 @@ If you choose to use MySQL, you must also add your database credentials:
```php
$MySql_username="USERNAME"; //your database username
$MySql_password="PASSWORD"; //your database password
$MySql_hostname="DB_HOSTNAME"; //database address, usually localhost\
$MySql_hostname="DB_HOSTNAME"; //database address, usually localhost
$MySql_databasename="DB_NAME"; //the name of the database where you loaded telemetry_mysql.sql
```
@ -373,8 +379,23 @@ w.postMessage('start {"telemetry_level":"basic"}')
Also, see example-telemetry.html
### Results sharing
This feature generates an image that can be share by the user containing the download, upload, ping, jitter and ISP (if enabled).
To use this feature, copy the `results` folder. You can customize the style of the generated image by editing the settings in `results/index.php`.
This feature requires Telemetry to be enabled, and FreeType2 must be installed in PHP (if not already be installed by your distro).
__Note:__ CSV doesn't currently support this.
__Important:__ This feature relies on PHP functions `imagefttext` and `imageftbbox` that are well known for being problematic. The most common problem is that they can't find the font files and therefore nothing is drawn. This problem is metioned [here](http://php.net/manual/en/function.imagefttext.php) and was experienced by a lot of users.
### Seeing the results
At the moment there is no front-end to see the telemetry data; you can connect to the database and see the collected results in the `speedtest_users` table.
A basic front-end for visualizing and searching tests by ID is available in `telemetry/stats.php`.
A login is required to access the interface. __Important__: change the default password in `telemetry_settings.php`.
__Note:__ CSV doesn't currently support this.
## Troubleshooting
These are the most common issues reported by users, and how to fix them. If you still need help, contact me at [info@fdossena.com](mailto:info@fdossena.com).
@ -400,6 +421,10 @@ Make sure your server is sending the ```Connection:keep-alive``` header
Edit getIP.php and replace lines 5-13 with what is more appropriate in your scenario.
Example: ```$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];```
#### The results sharing just generates a blank image
If the image doesn't display and the browser displays a broken image icon, FreeType2 is not installed or configured properly.
If the image is blank, this usually happens because PHP can't find the font files inside the `results` folder. You can fix your PHP config or edit `results/index.php` and use absolute paths for the fonts. This is a [known issue with PHP](http://php.net/manual/en/function.imagefttext.php) and no real solution is known.
## Known bugs and limitations
### General
* The ping/jitter test is measured by seeing how long it takes for an empty XHR to complete. It is not an acutal ICMP ping. Different browsers may also show different results, especially on very fast connections on slow devices.

View file

@ -0,0 +1,296 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no" />
<title>HTML5 Speedtest</title>
<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.testName{
position:absolute;
top:0.1em; left:0;
width:100%;
font-size:1.4em;
z-index:9;
}
div.meterText{
position:absolute;
bottom:1.55em; left:0;
width:100%;
font-size:2.5em;
z-index:9;
}
div.meterText:empty:before{
content:"0.00";
}
div.unit{
position:absolute;
bottom:2em; left:0;
width:100%;
z-index:9;
}
div.testArea canvas{
position:absolute;
top:0; left:0; width:100%; height:100%;
z-index:1;
}
div.testGroup{
display:inline-block;
}
#shareArea{
width:100%;
max-width:40em;
margin:0 auto;
margin-top:2em;
}
#shareArea > *{
display:block;
width:100%;
height:auto;
margin: 0.25em 0;
}
@media all and (max-width:65em){
body{
font-size:1.5vw;
}
}
@media all and (max-width:40em){
body{
font-size:0.8em;
}
div.testGroup{
display:block;
margin: 0 auto;
}
}
</style>
<script type="text/javascript">
function I(id){return document.getElementById(id);}
var meterBk="#E0E0E0";
var dlColor="#6060AA",
ulColor="#309030",
pingColor="#AA6060",
jitColor="#AA6060";
var progColor="#EEEEEE";
//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=16*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=16*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 msToAmount(s){
return 1-(1/(Math.pow(1.08,Math.sqrt(s))));
}
//SPEEDTEST AND UI CODE
var w=null; //speedtest worker
var data=null; //data from worker
var testParameters={
telemetry_level:"basic"
//Optional: add more test parameters here
}
function startStop(){
if(w!=null){
//speedtest is running, abort
w.postMessage('abort');
w=null;
data=null;
I("startStopBtn").className="";
initUI();
}else{
//test is not running, begin
w=new Worker('speedtest_worker.min.js');
w.postMessage('start '+JSON.stringify(testParameters));
I("startStopBtn").className="running";
I("shareArea").style.display="none";
w.onmessage=function(e){
data=e.data.split(';');
var status=Number(data[0]);
if(status>=4){
//test completed
I("startStopBtn").className="";
w=null;
updateUI(true);
if(status==4){
//if testId is present, show sharing panel, otherwise do nothing
try{
var testId=Number(data[9]);
if(!isNaN(testId)){
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="block";
}
}catch(e){}
}
}
};
}
}
//this function reads the data sent back by the worker and updates the UI
function updateUI(forced){
if(!forced&&(!data||!w)) return;
var status=Number(data[0]);
I("ip").textContent=data[4];
I("dlText").textContent=(status==1&&data[1]==0)?"...":data[1];
drawMeter(I("dlMeter"),mbpsToAmount(Number(data[1]*(status==1?oscillate():1))),meterBk,dlColor,Number(data[6]),progColor);
I("ulText").textContent=(status==3&&data[2]==0)?"...":data[2];
drawMeter(I("ulMeter"),mbpsToAmount(Number(data[2]*(status==3?oscillate():1))),meterBk,ulColor,Number(data[7]),progColor);
I("pingText").textContent=data[3];
drawMeter(I("pingMeter"),msToAmount(Number(data[3]*(status==2?oscillate():1))),meterBk,pingColor,Number(data[8]),progColor);
I("jitText").textContent=data[5];
drawMeter(I("jitMeter"),msToAmount(Number(data[5]*(status==2?oscillate():1))),meterBk,jitColor,Number(data[8]),progColor);
}
function oscillate(){
return 1+0.02*Math.sin(Date.now()/100);
}
//poll the status from the worker (this will call updateUI)
setInterval(function(){
if(w) w.postMessage('status');
},200);
//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);
drawMeter(I("pingMeter"),0,meterBk,pingColor,0);
drawMeter(I("jitMeter"),0,meterBk,jitColor,0);
I("dlText").textContent="";
I("ulText").textContent="";
I("pingText").textContent="";
I("jitText").textContent="";
I("ip").textContent="";
}
</script>
</head>
<body>
<h1>HTML5 Speedtest - Telemetry and sharing example</h1>
<div id="startStopBtn" onclick="startStop()"></div>
<div id="test">
<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">Mbps</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">Mbps</div>
</div>
</div>
<div class="testGroup">
<div class="testArea">
<div class="testName">Ping</div>
<canvas id="pingMeter" class="meter"></canvas>
<div id="pingText" class="meterText"></div>
<div class="unit">ms</div>
</div>
<div class="testArea">
<div class="testName">Jitter</div>
<canvas id="jitMeter" class="meter"></canvas>
<div id="jitText" class="meterText"></div>
<div class="unit">ms</div>
</div>
</div>
<div id="ipArea">
IP Address: <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()"/>
<img src="" id="resultsImg" />
</div>
</div>
<div>Basic telemetry is active; results will be saved in your database, without the full log. If the results don't appear, or the sharing panel doesn't appear at the end of the test, check the settings in telemetry_settings.php</div>
<a href="https://github.com/adolfintel/speedtest">Source code</a>
<script type="text/javascript">setTimeout(initUI,100);</script>
</body>
</html>

View file

@ -187,7 +187,7 @@ function initUI(){
IP Address: <span id="ip"></span>
</div>
</div>
<div>Basic telemetry is active; results will be saved in your database, without the full log. If the results don't appear, check the settings in telemetry.php</div>
<div>Basic telemetry is active; results will be saved in your database, without the full log. If the results don't appear, check the settings in telemetry_settings.php</div>
<a href="https://github.com/adolfintel/speedtest">Source code</a>
<script type="text/javascript">initUI();</script>
</body>

View file

@ -1,7 +1,11 @@
<?php
/*
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, Contry 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).
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.
*/
$ip = "";
header('Content-Type: text/plain; charset=utf-8');
header('Content-Type: application/json; charset=utf-8');
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['X-Real-IP'])) {
@ -15,11 +19,11 @@ if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = preg_replace("/^::ffff:/", "", $ip);
if (strpos($ip, '::1') !== false) {
echo $ip . " - localhost ipv6 access";
echo json_encode(['processedString' => $ip . " - localhost ipv6 access", 'rawIspInfo' => ""]);
die();
}
if (strpos($ip, '127.0.0') !== false) {
echo $ip . " - localhost ipv4 access";
echo json_encode(['processedString' => $ip . " - localhost ipv4 access", 'rawIspInfo' => ""]);
die();
}
@ -42,9 +46,11 @@ function distance($latitudeFrom, $longitudeFrom, $latitudeTo, $longitudeTo) {
if (isset($_GET["isp"])) {
$isp = "";
$rawIspInfo=null;
try {
$json = file_get_contents("https://ipinfo.io/" . $ip . "/json");
$details = json_decode($json, true);
$rawIspInfo=$details;
if (array_key_exists("org", $details))
$isp .= $details["org"];
else
@ -87,8 +93,8 @@ if (isset($_GET["isp"])) {
} catch (Exception $ex) {
$isp = "Unknown ISP";
}
echo $ip . " - " . $isp;
echo json_encode(['processedString' => $ip . " - " . $isp, 'rawIspInfo' => $rawIspInfo]);
} else {
echo $ip;
echo json_encode(['processedString' => $ip, 'rawIspInfo' => ""]);
}
?>

BIN
results/OpenSans-Light.ttf Normal file

Binary file not shown.

Binary file not shown.

123
results/index.php Normal file
View file

@ -0,0 +1,123 @@
<?php
error_reporting(0);
putenv('GDFONTPATH=' . realpath('.'));
$SCALE=1.25;
$WIDTH=530*$SCALE;
$HEIGHT=150*$SCALE;
$im=imagecreatetruecolor($WIDTH,$HEIGHT);
$BACKGROUND_COLOR=imagecolorallocate($im,248,248,248);
$FONT_1="OpenSans-Semibold";
$FONT_1_SIZE=16*$SCALE;
$FONT_2="OpenSans-Light";
$FONT_2_SIZE=24*$SCALE;
$FONT_3="OpenSans-Semibold";
$FONT_3_SIZE=14*$SCALE;
$FONT_4="OpenSans-Semibold";
$FONT_4_SIZE=10*$SCALE;
$FONT_WATERMARK="OpenSans-Light";
$FONT_WATERMARK_SIZE=8*$SCALE;
$TEXT_COLOR_1=imagecolorallocate($im,40,40,40);
$TEXT_COLOR_2=imagecolorallocate($im,96,96,96);
$TEXT_COLOR_3=imagecolorallocate($im,40,40,40);
$TEXT_COLOR_4=imagecolorallocate($im,40,40,40);
$TEXT_COLOR_WATERMARK=imagecolorallocate($im,160,160,160);
$POSITION_Y_1=24*$SCALE;
$POSITION_Y_2=78*$SCALE;
$POSITION_Y_3=118*$SCALE;
$POSITION_Y_4=146*$SCALE;
$POSITION_Y_WATERMARK=146*$SCALE;
$POSITION_X_DL=68*$SCALE;
$POSITION_X_UL=200*$SCALE;
$POSITION_X_PING=330*$SCALE;
$POSITION_X_JIT=460*$SCALE;
$POSITION_X_ISP=4*$SCALE;
$DL_TEXT="Download";
$UL_TEXT="Upload";
$PING_TEXT="Ping";
$JIT_TEXT="Jitter";
$MBPS_TEXT="Mbps";
$MS_TEXT="ms";
$WATERMARK_TEXT="HTML5 Speedtest";
$id=$_GET["id"];
include_once('../telemetry/telemetry_settings.php');
$conn=null; $q=null;
$ispinfo=null; $dl=null; $ul=null; $ping=null; $jit=null;
if($db_type=="mysql"){
$conn = new mysqli($MySql_hostname, $MySql_username, $MySql_password, $MySql_databasename);
$q = $conn->prepare("select ispinfo,dl,ul,ping,jitter from speedtest_users where id=?");
$q->bind_param("i",$id);
$q->execute();
$q->bind_result($ispinfo,$dl,$ul,$ping,$jit);
$q->fetch();
}else if($db_type=="sqlite"){
$conn = new PDO("sqlite:$Sqlite_db_file") or die();
$q=$conn->prepare("select ispinfo,dl,ul,ping,jitter from speedtest_users where id=?") or die();
$q->execute(array($id)) or die();
$row=$q->fetch() or die();
$ispinfo=$row["ispinfo"];
$dl=$row["dl"];
$ul=$row["ul"];
$ping=$row["ping"];
$jitter=$row["jitter"];
$conn=null;
}else if($db_type=="postgresql"){
$conn_host = "host=$PostgreSql_hostname";
$conn_db = "dbname=$PostgreSql_databasename";
$conn_user = "user=$PostgreSql_username";
$conn_password = "password=$PostgreSql_password";
$conn = new PDO("pgsql:$conn_host;$conn_db;$conn_user;$conn_password") or die();
$q=$conn->prepare("select ispinfo,dl,ul,ping,jitter from speedtest_users where id=?") or die();
$q->execute(array($id)) or die();
$row=$q->fetch() or die();
$ispinfo=$row["ispinfo"];
$dl=$row["dl"];
$ul=$row["ul"];
$ping=$row["ping"];
$jitter=$row["jitter"];
$conn=null;
}else die();
$ispinfo=json_decode($ispinfo,true)["processedString"];
$dash=strrpos($ispinfo,"-");
if(!($dash===FALSE)){
$ispinfo=substr($ispinfo,$dash+2);
$par=strrpos($ispinfo,"(");
if(!($par===FALSE)) $ispinfo=substr($ispinfo,0,$par);
}else $ispinfo="";
$dlBbox=imageftbbox($FONT_1_SIZE,0,$FONT_1,$DL_TEXT);
$ulBbox=imageftbbox($FONT_1_SIZE,0,$FONT_1,$UL_TEXT);
$pingBbox=imageftbbox($FONT_1_SIZE,0,$FONT_1,$PING_TEXT);
$jitBbox=imageftbbox($FONT_1_SIZE,0,$FONT_1,$JIT_TEXT);
$dlMeterBbox=imageftbbox($FONT_2_SIZE,0,$FONT_2,$dl);
$ulMeterBbox=imageftbbox($FONT_2_SIZE,0,$FONT_2,$ul);
$pingMeterBbox=imageftbbox($FONT_2_SIZE,0,$FONT_2,$ping);
$jitMeterBbox=imageftbbox($FONT_2_SIZE,0,$FONT_2,$jit);
$mbpsBbox=imageftbbox($FONT_3_SIZE,0,$FONT_3,$MBPS_TEXT);
$msBbox=imageftbbox($FONT_3_SIZE,0,$FONT_3,$MS_TEXT);
$watermarkBbox=imageftbbox($FONT_WATERMARK_SIZE,0,$FONT_WATERMARK,$WATERMARK_TEXT);
$POSITION_X_WATERMARK=$WIDTH-$watermarkBbox[4]-4*$SCALE;
imagefilledrectangle($im, 0, 0, $WIDTH, $HEIGHT, $BACKGROUND_COLOR);
imagefttext($im,$FONT_1_SIZE,0,$POSITION_X_DL-$dlBbox[4]/2,$POSITION_Y_1,$TEXT_COLOR_1,$FONT_1,$DL_TEXT);
imagefttext($im,$FONT_1_SIZE,0,$POSITION_X_UL-$ulBbox[4]/2,$POSITION_Y_1,$TEXT_COLOR_1,$FONT_1,$UL_TEXT);
imagefttext($im,$FONT_1_SIZE,0,$POSITION_X_PING-$pingBbox[4]/2,$POSITION_Y_1,$TEXT_COLOR_1,$FONT_1,$PING_TEXT);
imagefttext($im,$FONT_1_SIZE,0,$POSITION_X_JIT-$jitBbox[4]/2,$POSITION_Y_1,$TEXT_COLOR_1,$FONT_1,$JIT_TEXT);
imagefttext($im,$FONT_2_SIZE,0,$POSITION_X_DL-$dlMeterBbox[4]/2,$POSITION_Y_2,$TEXT_COLOR_2,$FONT_2,$dl);
imagefttext($im,$FONT_2_SIZE,0,$POSITION_X_UL-$ulMeterBbox[4]/2,$POSITION_Y_2,$TEXT_COLOR_2,$FONT_2,$ul);
imagefttext($im,$FONT_2_SIZE,0,$POSITION_X_PING-$pingMeterBbox[4]/2,$POSITION_Y_2,$TEXT_COLOR_2,$FONT_2,$ping);
imagefttext($im,$FONT_2_SIZE,0,$POSITION_X_JIT-$jitMeterBbox[4]/2,$POSITION_Y_2,$TEXT_COLOR_2,$FONT_2,$jit);
imagefttext($im,$FONT_3_SIZE,0,$POSITION_X_DL-$mbpsBbox[4]/2,$POSITION_Y_3,$TEXT_COLOR_3,$FONT_3,$MBPS_TEXT);
imagefttext($im,$FONT_3_SIZE,0,$POSITION_X_UL-$mbpsBbox[4]/2,$POSITION_Y_3,$TEXT_COLOR_3,$FONT_3,$MBPS_TEXT);
imagefttext($im,$FONT_3_SIZE,0,$POSITION_X_PING-$msBbox[4]/2,$POSITION_Y_3,$TEXT_COLOR_3,$FONT_3,$MS_TEXT);
imagefttext($im,$FONT_3_SIZE,0,$POSITION_X_JIT-$msBbox[4]/2,$POSITION_Y_3,$TEXT_COLOR_3,$FONT_3,$MS_TEXT);
imagefttext($im,$FONT_4_SIZE,0,$POSITION_X_ISP,$POSITION_Y_4,$TEXT_COLOR_4,$FONT_4,$ispinfo);
imagefttext($im,$FONT_WATERMARK_SIZE,0,$POSITION_X_WATERMARK,$POSITION_Y_WATERMARK,$TEXT_COLOR_WATERMARK,$FONT_WATERMARK,$WATERMARK_TEXT);
header('Content-Type: image/png');
imagepng($im);
imagedestroy($im);
?>

View file

@ -1,5 +1,5 @@
/*
HTML5 Speedtest v4.5.5
HTML5 Speedtest v4.6
by Federico Dossena
https://github.com/adolfintel/speedtest/
GNU LGPLv3 License
@ -15,6 +15,12 @@ var clientIp = '' // client's IP address as reported by getIP.php
var dlProgress = 0 //progress of download test 0-1
var ulProgress = 0 //progress of upload test 0-1
var pingProgress = 0 //progress of ping+jitter test 0-1
var testId = 'noID' //test ID (sent back by telemetry if used, the string 'noID' otherwise)
var HTML_ESCAPE_MAP={'&': '&amp;','<': '&lt;','>': '&gt;','"': '&quot;',"'": '&#039;'};
String.prototype.escapeHtml=function(){
return this.replace(/[&<>"']/g, function(m){return HTML_ESCAPE_MAP[m]});
}
var log='' //telemetry log
function tlog(s){log+=Date.now()+': '+s+'\n'}
@ -46,7 +52,7 @@ var settings = {
overheadCompensationFactor: 1.06, //can be changed to compensatie for transport overhead. (see doc.md for some other values)
useMebibits: false, //if set to true, speed will be reported in mebibits/s instead of megabits/s
telemetry_level: 0, // 0=disabled, 1=basic (results only), 2=full (results+log)
url_telemetry: 'telemetry.php' // path to the script that adds telemetry data to the database
url_telemetry: 'telemetry/telemetry.php' // path to the script that adds telemetry data to the database
}
var xhr = null // array of currently active xhr requests
@ -69,7 +75,7 @@ function url_sep (url) { return url.match(/\?/) ? '&' : '?'; }
this.addEventListener('message', function (e) {
var params = e.data.split(' ')
if (params[0] === 'status') { // return status
postMessage(testStatus + ';' + dlStatus + ';' + ulStatus + ';' + pingStatus + ';' + clientIp + ';' + jitterStatus + ';' + dlProgress + ';' + ulProgress + ';' + pingProgress)
postMessage(testStatus + ';' + dlStatus + ';' + ulStatus + ';' + pingStatus + ';' + clientIp + ';' + jitterStatus + ';' + dlProgress + ';' + ulProgress + ';' + pingProgress+ ';' + testId)
}
if (params[0] === 'start' && testStatus === -1) { // start new test
testStatus = 0
@ -125,7 +131,12 @@ this.addEventListener('message', function (e) {
var iRun=false,dRun=false,uRun=false,pRun=false;
var runNextTest=function(){
if(testStatus==5) return;
if(test_pointer>=settings.test_order.length){testStatus=4; sendTelemetry(); return;}
if(test_pointer>=settings.test_order.length){ //test is finished
if(settings.telemetry_level>0)
sendTelemetry(function(id){testStatus=4; if(id!=-1)testId=id})
else testStatus=4
return;
}
switch(settings.test_order.charAt(test_pointer)){
case 'I':{test_pointer++; if(iRun) {runNextTest(); return;} else iRun=true; getIp(runNextTest);} break;
case 'D':{test_pointer++; if(dRun) {runNextTest(); return;} else dRun=true; testStatus=1; dlTest(runNextTest);} break;
@ -142,7 +153,7 @@ this.addEventListener('message', function (e) {
clearRequests() // stop all xhr activity
runNextTest=null;
if (interval) clearInterval(interval) // clear timer if present
if (settings.telemetry_level > 1) sendTelemetry()
if (settings.telemetry_level > 1) sendTelemetry(function(){})
testStatus = 5; dlStatus = ''; ulStatus = ''; pingStatus = ''; jitterStatus = '' // set test as aborted
}
})
@ -161,13 +172,21 @@ function clearRequests () {
}
// gets client's IP using url_getIp, then calls the done function
var ipCalled = false // used to prevent multiple accidental calls to getIp
var ispInfo=""; //used for telemetry
function getIp (done) {
tlog('getIp')
if (ipCalled) return; else ipCalled = true // getIp already called?
xhr = new XMLHttpRequest()
xhr.onload = function () {
tlog("IP: "+xhr.responseText)
clientIp = xhr.responseText
try{
var data=JSON.parse(xhr.responseText)
clientIp=data.processedString.escapeHtml()
ispInfo=data.rawIspInfo
}catch(e){
clientIp = xhr.responseText.escapeHtml()
ispInfo=''
}
done()
}
xhr.onerror = function () {
@ -449,15 +468,31 @@ function pingTest (done) {
doPing() // start first ping
}
// telemetry
function sendTelemetry(){
function sendTelemetry(done){
if (settings.telemetry_level < 1) return
xhr = new XMLHttpRequest()
xhr.onload = function () { console.log('TELEMETRY OL '+xhr.responseText) }
xhr.onerror = function () { console.log('TELEMETRY ERROR '+xhr) }
xhr.onload = function () {
try{
var parts=xhr.responseText.split(' ')
if(parts[0]=='id'){
try{
var id=Number(parts[1])
if(!isNaN(id)) done(id); else done(-1);
}catch(e){done(-1)}
} else done(-1);
}catch(e){
done(-1)
}
}
xhr.onerror = function () { console.log('TELEMETRY ERROR '+xhr); done(-1) }
xhr.open('POST', settings.url_telemetry+url_sep(settings.url_telemetry)+"r="+Math.random(), true);
var telemetryIspInfo={
processedString: clientIp,
rawIspInfo: (typeof ispInfo === "object")?ispInfo:""
}
try{
var fd = new FormData()
fd.append('ispinfo', clientIp) //clientIp also contains ISP info
fd.append('ispinfo', JSON.stringify(telemetryIspInfo));
fd.append('dl', dlStatus)
fd.append('ul', ulStatus)
fd.append('ping', pingStatus)
@ -465,7 +500,7 @@ function sendTelemetry(){
fd.append('log', settings.telemetry_level>1?log:"")
xhr.send(fd)
}catch(ex){
var postData = 'ispinfo='+encodeURIComponent(clientIp)+'dl='+encodeURIComponent(dlStatus)+'&ul='+encodeURIComponent(ulStatus)+'&ping='+encodeURIComponent(pingStatus)+'&jitter='+encodeURIComponent(jitterStatus)+'&log='+encodeURIComponent(settings.telemetry_level>1?log:'')
var postData = 'ispinfo='+encodeURIComponent(JSON.stringify(telemetryIspInfo))+'&dl='+encodeURIComponent(dlStatus)+'&ul='+encodeURIComponent(ulStatus)+'&ping='+encodeURIComponent(pingStatus)+'&jitter='+encodeURIComponent(jitterStatus)+'&log='+encodeURIComponent(settings.telemetry_level>1?log:'')
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.send(postData)
}

File diff suppressed because one or more lines are too long

143
telemetry/stats.php Normal file
View file

@ -0,0 +1,143 @@
<?php
session_start();
error_reporting(0);
header('Content-Type: text/html; charset=utf-8');
?>
<!DOCTYPE html>
<html>
<head>
<title>HTML5 Speedtest - Stats</title>
<style type="text/css">
html,body{
margin:0;
padding:0;
border:none;
width:100%; min-height:100%;
}
html{
background-color:#304090;
font-family:Sans-Serif;
}
body{
background-color:#FFFFFF;
box-sizing:border-box;
width:100%;
max-width:70em;
margin:4em auto;
box-shadow:0 0 20em #00000040;
padding:1em 1em 4em 1em;
border-radius:0.4em;
}
table{
margin:4em 0;
}
</style>
</head>
<body>
<h1>HTML5 Speedtest - Stats</h1>
<?php
include_once("telemetry_settings.php");
if($stats_password=="PASSWORD"){
?>
Please set $stats_password in telemetry_settings.php to enable access.
<?php
}else if($_SESSION["logged"]===true){
if($_GET["op"]=="logout"){
$_SESSION["logged"]=false;
?><script type="text/javascript">window.location.search=""</script><?php
}else{
$conn=null;
if($db_type=="mysql"){
$conn = new mysqli($MySql_hostname, $MySql_username, $MySql_password, $MySql_databasename);
}else if($db_type=="sqlite"){
$conn = new PDO("sqlite:$Sqlite_db_file");
} else if($db_type=="postgresql"){
$conn_host = "host=$PostgreSql_hostname";
$conn_db = "dbname=$PostgreSql_databasename";
$conn_user = "user=$PostgreSql_username";
$conn_password = "password=$PostgreSql_password";
$conn = new PDO("pgsql:$conn_host;$conn_db;$conn_user;$conn_password");
}else die();
?>
<form action="stats.php?op=logout" method="POST"><input type="submit" value="Logout" /></form>
<form action="stats.php?op=id" method="POST">
<h3>Search test results</h6>
<input type="text" name="id" id="id" placeholder="Test ID" value=""/>
<input type="submit" value="Find" />
<input type="submit" onclick="document.getElementById('id').value=''" value="Show last 100 tests" />
</form>
<?php
$q=null;
if($_GET["op"]=="id"&&!empty($_POST["id"])){
$id=$_POST["id"];
if($db_type=="mysql"){
$q=$conn->prepare("select id,timestamp,ip,ispinfo,ua,lang,dl,ul,ping,jitter,log from speedtest_users where id=?");
$q->bind_param("i",$_POST["id"]);
$q->execute();
$q->bind_result($id,$timestamp,$ip,$ispinfo,$ua,$lang,$dl,$ul,$ping,$jitter,$log);
} else if($db_type=="sqlite"||$db_type=="postgresql"){
$q=$conn->prepare("select id,timestamp,ip,ispinfo,ua,lang,dl,ul,ping,jitter,log from speedtest_users where id=?");
$q->execute(array($id));
} else die();
}else{
if($db_type=="mysql"){
$q=$conn->prepare("select id,timestamp,ip,ispinfo,ua,lang,dl,ul,ping,jitter,log from speedtest_users order by timestamp desc limit 0,100");
$q->execute();
$q->bind_result($id,$timestamp,$ip,$ispinfo,$ua,$lang,$dl,$ul,$ping,$jitter,$log);
} else if($db_type=="sqlite"||$db_type=="postgresql"){
$q=$conn->prepare("select id,timestamp,ip,ispinfo,ua,lang,dl,ul,ping,jitter,log from speedtest_users order by timestamp desc limit 0,100");
$q->execute();
}else die();
}
while(true){
$id=null; $timestamp=null; $ip=null; $ispinfo=null; $ua=null; $lang=null; $dl=null; $ul=null; $ping=null; $jitter=null; $log=null;
if($db_type=="mysql"){
if(!$q->fetch()) break;
} else if($db_type=="sqlite"||$db_type=="postgresql"){
if(!($row=$q->fetch())) break;
$id=$row["id"];
$timestamp=$row["timestamp"];
$ip=$row["ip"];
$ispinfo=$row["ispinfo"];
$ua=$row["ua"];
$lang=$row["lang"];
$dl=$row["dl"];
$ul=$row["ul"];
$ping=$row["ping"];
$jitter=$row["jitter"];
$log=$row["log"];
}else die();
?>
<table>
<tr><th>Test ID</th><td><?=htmlspecialchars($id, ENT_HTML5, 'UTF-8') ?></td></tr>
<tr><th>Date and time</th><td><?=htmlspecialchars($timestamp, ENT_HTML5, 'UTF-8') ?></td></tr>
<tr><th>IP and ISP Info</th><td><?=$ip ?><br/><?=htmlspecialchars($ispinfo, ENT_HTML5, 'UTF-8') ?></td></tr>
<tr><th>User agent and locale</th><td><?=$ua ?><br/><?=htmlspecialchars($lang, ENT_HTML5, 'UTF-8') ?></td></tr>
<tr><th>Download speed</th><td><?=htmlspecialchars($dl, ENT_HTML5, 'UTF-8') ?></td></tr>
<tr><th>Upload speed</th><td><?=htmlspecialchars($ul, ENT_HTML5, 'UTF-8') ?></td></tr>
<tr><th>Ping</th><td><?=htmlspecialchars($ping, ENT_HTML5, 'UTF-8') ?></td></tr>
<tr><th>Jitter</th><td><?=htmlspecialchars($jitter, ENT_HTML5, 'UTF-8') ?></td></tr>
<tr><th>Log</th><td><?=htmlspecialchars($log, ENT_HTML5, 'UTF-8') ?></td></tr>
</table>
<?php
}
?>
<?php
}
}else{
if($_GET["op"]=="login"&&$_POST["password"]===$stats_password){
$_SESSION["logged"]=true;
?><script type="text/javascript">window.location.search=""</script><?php
}else{
?>
<form action="stats.php?op=login" method="POST">
<h3>Login</h3>
<input type="password" name="password" placeholder="Password" value=""/>
<input type="submit" value="Login" />
</form>
<?php
}
}
?>
</body>
</html>

View file

@ -15,8 +15,9 @@ if($db_type=="mysql"){
$conn = new mysqli($MySql_hostname, $MySql_username, $MySql_password, $MySql_databasename) or die("1");
$stmt = $conn->prepare("INSERT INTO speedtest_users (ip,ispinfo,ua,lang,dl,ul,ping,jitter,log) VALUES (?,?,?,?,?,?,?,?,?)") or die("2");
$stmt->bind_param("sssssssss",$ip,$ispinfo,$ua,$lang,$dl,$ul,$ping,$jitter,$log) or die("3");
$stmt->execute() or die("4");
$stmt->execute() or die("4");
$stmt->close() or die("5");
echo "id ".$conn->insert_id;
$conn->close() or die("6");
}elseif($db_type=="sqlite"){
@ -38,6 +39,7 @@ if($db_type=="mysql"){
");
$stmt = $conn->prepare("INSERT INTO speedtest_users (ip,ispinfo,ua,lang,dl,ul,ping,jitter,log) VALUES (?,?,?,?,?,?,?,?,?)") or die("2");
$stmt->execute(array($ip,$ispinfo,$ua,$lang,$dl,$ul,$ping,$jitter,$log)) or die("3");
echo "id ".$conn->lastInsertId();
$conn = null;
}elseif($db_type=="postgresql"){
// Prepare connection parameters for db connection
@ -49,6 +51,7 @@ if($db_type=="mysql"){
$conn = new PDO("pgsql:$conn_host;$conn_db;$conn_user;$conn_password") or die("1");
$stmt = $conn->prepare("INSERT INTO speedtest_users (ip,ispinfo,ua,lang,dl,ul,ping,jitter,log) VALUES (?,?,?,?,?,?,?,?,?)") or die("2");
$stmt->execute(array($ip,$ispinfo,$ua,$lang,$dl,$ul,$ping,$jitter,$log)) or die("3");
echo "id ".$conn->lastInsertId();
$conn = null;
}
elseif($db_type=="csv"){

View file

@ -1,9 +1,10 @@
<?php
$db_type="mysql"; //Type of db: "mysql", "sqlite", "postgresql" or "csv"
$stats_password="PASSWORD"; //password to login to stats.php. Change this!!!
// Sqlite3 settings
$Sqlite_db_file = "../telemetry.sql";
$Sqlite_db_file = "../../telemetry.sql";
// Mysql settings
$MySql_username="USERNAME";
@ -18,7 +19,10 @@ $PostgreSql_hostname="DB_HOSTNAME";
$PostgreSql_databasename="DB_NAME";
// CSV settings
$Csv_File="reports.csv";
$Csv_File="../../reports.csv";
$timezone='UTC';
?>
//IMPORTANT: DO NOT ADD ANYTHING BELOW THIS PHP CLOSING TAGS, NOT EVEN EMPTY LINES!
?>