mirror of
https://github.com/billz/raspap-webgui.git
synced 2025-12-26 23:26:47 +01:00
Add speedtest dependencies
This commit is contained in:
189
app/js/speedtestUI.js
Normal file
189
app/js/speedtestUI.js
Normal file
@@ -0,0 +1,189 @@
|
||||
function I(i){return document.getElementById(i);}
|
||||
|
||||
const origin=window.location.origin;
|
||||
const host=window.location.host;
|
||||
var SPEEDTEST_SERVERS=[
|
||||
{
|
||||
"name":"RaspAP Speedtest server (US)",
|
||||
"server":"https://speedtest.raspap.com/",
|
||||
"dlURL":"backend/garbage.php",
|
||||
"ulURL":"backend/empty.php",
|
||||
"pingURL":"backend/empty.php",
|
||||
"getIpURL":"backend/getIP.php"
|
||||
},
|
||||
{
|
||||
"name":"RaspAP ("+host+")",
|
||||
"server":origin,
|
||||
"dlURL":"dist/speedtest/backend/garbage.php",
|
||||
"ulURL":"dist/speedtest/backend/empty.php",
|
||||
"pingURL":"dist/speedtest/backend/empty.php",
|
||||
"getIpURL":"dist/speedtest/backend/getIP.php"
|
||||
}
|
||||
];
|
||||
|
||||
//INITIALIZE SPEEDTEST
|
||||
var s=new Speedtest(); //create speedtest object
|
||||
s.setParameter("telemetry_level","basic"); //enable telemetry
|
||||
|
||||
//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();
|
||||
}
|
||||
}
|
||||
|
||||
var meterBk=/Trident.*rv:(\d+\.\d+)/i.test(navigator.userAgent)?"#EAEAEA":"#80808040";
|
||||
var dlColor="#4BC0C0",
|
||||
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){
|
||||
//speedtest is running, abort
|
||||
s.abort();
|
||||
data=null;
|
||||
I("startStopBtn").className="btn btn-outline btn-primary";
|
||||
I("server").disabled=false;
|
||||
initUI();
|
||||
}else{
|
||||
//test is not running, begin
|
||||
I("startStopBtn").className="btn btn-outline btn-primary running";
|
||||
I("server").disabled=true;
|
||||
s.onupdate=function(data){
|
||||
uiData=data;
|
||||
};
|
||||
s.onend=function(aborted){
|
||||
I("startStopBtn").className="btn btn-outline btn-primary";
|
||||
I("server").disabled=false;
|
||||
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="";
|
||||
}
|
||||
// add EventListener to diagnostic tab
|
||||
var e = document.querySelectorAll("a[href^='#diagnostic']");
|
||||
e[0].addEventListener("click", function(){
|
||||
initServers()
|
||||
});
|
||||
|
||||
14
dist/speedtest/backend/empty.php
vendored
Executable file
14
dist/speedtest/backend/empty.php
vendored
Executable file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
header('HTTP/1.1 200 OK');
|
||||
|
||||
if (isset($_GET['cors'])) {
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST');
|
||||
header('Access-Control-Allow-Headers: Content-Encoding, Content-Type');
|
||||
}
|
||||
|
||||
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');
|
||||
header('Connection: keep-alive');
|
||||
66
dist/speedtest/backend/garbage.php
vendored
Executable file
66
dist/speedtest/backend/garbage.php
vendored
Executable file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
// Disable Compression
|
||||
@ini_set('zlib.output_compression', 'Off');
|
||||
@ini_set('output_buffering', 'Off');
|
||||
@ini_set('output_handler', '');
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
function getChunkCount()
|
||||
{
|
||||
if (!array_key_exists('ckSize', $_GET)
|
||||
|| !ctype_digit($_GET['ckSize'])
|
||||
|| (int) $_GET['ckSize'] <= 0
|
||||
) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
if ((int) $_GET['ckSize'] > 1024) {
|
||||
return 1024;
|
||||
}
|
||||
|
||||
return (int) $_GET['ckSize'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
function sendHeaders()
|
||||
{
|
||||
header('HTTP/1.1 200 OK');
|
||||
|
||||
if (isset($_GET['cors'])) {
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST');
|
||||
}
|
||||
|
||||
// Indicate a file download
|
||||
header('Content-Description: File Transfer');
|
||||
header('Content-Type: application/octet-stream');
|
||||
header('Content-Disposition: attachment; filename=random.dat');
|
||||
header('Content-Transfer-Encoding: binary');
|
||||
|
||||
// Cache settings: never cache this request
|
||||
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');
|
||||
}
|
||||
|
||||
// Determine how much data we should send
|
||||
$chunks = getChunkCount();
|
||||
|
||||
// Generate data
|
||||
if (function_exists('random_bytes')) {
|
||||
$data = random_bytes(1048576);
|
||||
} else {
|
||||
$data = openssl_random_pseudo_bytes(1048576);
|
||||
}
|
||||
|
||||
// Deliver chunks of 1048576 bytes
|
||||
sendHeaders();
|
||||
for ($i = 0; $i < $chunks; $i++) {
|
||||
echo $data;
|
||||
flush();
|
||||
}
|
||||
325
dist/speedtest/backend/getIP.php
vendored
Executable file
325
dist/speedtest/backend/getIP.php
vendored
Executable file
@@ -0,0 +1,325 @@
|
||||
<?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, 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).
|
||||
* 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.
|
||||
*/
|
||||
|
||||
error_reporting(0);
|
||||
|
||||
define('API_KEY_FILE', 'getIP_ipInfo_apikey.php');
|
||||
define('SERVER_LOCATION_CACHE_FILE', 'getIP_serverLocation.php');
|
||||
|
||||
require_once 'getIP_util.php';
|
||||
|
||||
/**
|
||||
* @param string $ip
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
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';
|
||||
}
|
||||
|
||||
// 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 '';
|
||||
}
|
||||
|
||||
include API_KEY_FILE;
|
||||
|
||||
if (empty($IPINFO_APIKEY)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return '?token='.$IPINFO_APIKEY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $ip
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
function getIspInfo($ip)
|
||||
{
|
||||
$json = file_get_contents('https://ipinfo.io/'.$ip.'/json'.getIpInfoTokenString());
|
||||
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)
|
||||
|| !array_key_exists('org', $rawIspInfo)
|
||||
|| !is_string($rawIspInfo['org'])
|
||||
|| empty($rawIspInfo['org'])
|
||||
) {
|
||||
return 'Unknown ISP';
|
||||
}
|
||||
|
||||
// Remove AS##### from ISP name, if present
|
||||
return preg_replace('/AS\\d+\\s/', '', $rawIspInfo['org']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
function getServerLocation()
|
||||
{
|
||||
$serverLoc = null;
|
||||
if (file_exists(SERVER_LOCATION_CACHE_FILE)
|
||||
&& is_readable(SERVER_LOCATION_CACHE_FILE)
|
||||
) {
|
||||
include SERVER_LOCATION_CACHE_FILE;
|
||||
}
|
||||
if (is_string($serverLoc) && !empty($serverLoc)) {
|
||||
return $serverLoc;
|
||||
}
|
||||
|
||||
$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(
|
||||
[
|
||||
'processedString' => $processedString,
|
||||
'rawIspInfo' => $rawIspInfo ?: '',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$ip = getClientIp();
|
||||
|
||||
$localIpInfo = getLocalOrPrivateIpInfo($ip);
|
||||
// local ip, no need to fetch further information
|
||||
if (is_string($localIpInfo)) {
|
||||
sendResponse($ip, $localIpInfo);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!isset($_GET['isp'])) {
|
||||
sendResponse($ip);
|
||||
exit;
|
||||
}
|
||||
|
||||
$rawIspInfo = getIspInfo($ip);
|
||||
$isp = getIsp($rawIspInfo);
|
||||
$distance = getDistance($rawIspInfo);
|
||||
|
||||
sendResponse($ip, $isp, $distance, $rawIspInfo);
|
||||
4
dist/speedtest/backend/getIP_ipInfo_apikey.php
vendored
Executable file
4
dist/speedtest/backend/getIP_ipInfo_apikey.php
vendored
Executable file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
|
||||
// put your token between the quotes if you have one
|
||||
$IPINFO_APIKEY = '';
|
||||
3
dist/speedtest/backend/getIP_serverLocation.php
vendored
Normal file
3
dist/speedtest/backend/getIP_serverLocation.php
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
$serverLoc = '45.9237,6.8693';
|
||||
21
dist/speedtest/backend/getIP_util.php
vendored
Executable file
21
dist/speedtest/backend/getIP_util.php
vendored
Executable file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
function getClientIp()
|
||||
{
|
||||
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
$ip = $_SERVER['HTTP_CLIENT_IP'];
|
||||
} elseif (!empty($_SERVER['HTTP_X_REAL_IP'])) {
|
||||
$ip = $_SERVER['HTTP_X_REAL_IP'];
|
||||
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||||
$ip = preg_replace('/,.*/', '', $ip); // hosts are comma-separated, client is first
|
||||
} else {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
|
||||
return preg_replace('/^::ffff:/', '', $ip);
|
||||
}
|
||||
|
||||
231
dist/speedtest/speedtest.css
vendored
Normal file
231
dist/speedtest/speedtest.css
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
#testWrapper {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#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;
|
||||
transition: all 0.3s;
|
||||
box-sizing: border-box;
|
||||
width: 8em;
|
||||
line-height: 2.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#startStopBtn.running {
|
||||
background-color: #FF3030;
|
||||
border-color: #FF3030;
|
||||
color: #FFFFFF;
|
||||
line-height: 2.5em;
|
||||
}
|
||||
|
||||
#startStopBtn:before {
|
||||
content: "Start";
|
||||
}
|
||||
|
||||
#startStopBtn.running:before {
|
||||
content: "Abort";
|
||||
}
|
||||
|
||||
#serverArea {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
#server {
|
||||
font-size: 1em;
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
#test {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
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.3em;
|
||||
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;
|
||||
margin: 0 auto;
|
||||
width: 50%;
|
||||
height: auto;
|
||||
box-shadow: 0 0 3em 1em #333;
|
||||
z-index: 999999;
|
||||
text-align: left;
|
||||
background-color: #FFFFFF;
|
||||
padding: 1em;
|
||||
border-radius: 0.3em;
|
||||
color: #858796;
|
||||
}
|
||||
|
||||
#privacyPolicy h4, h5 {
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
a.privacy {
|
||||
text-align: center;
|
||||
font-size: 0.8em;
|
||||
color: #808080;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media all and (max-width:40em) {
|
||||
body {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
div.visible {
|
||||
animation: fadeIn 0.4s;
|
||||
display: block;
|
||||
}
|
||||
|
||||
div.hidden {
|
||||
animation: fadeOut 0.4s;
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.centered {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
0% {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
327
dist/speedtest/speedtest.js
vendored
Executable file
327
dist/speedtest/speedtest.js
vendored
Executable file
@@ -0,0 +1,327 @@
|
||||
/*
|
||||
LibreSpeed - Main
|
||||
by Federico Dossena
|
||||
https://github.com/librespeed/speedtest/
|
||||
GNU LGPLv3 License
|
||||
*/
|
||||
|
||||
function Speedtest() {
|
||||
this._serverList = []; //when using multiple points of test, this is a list of test points
|
||||
this._selectedServer = null; //when using multiple points of test, this is the selected server
|
||||
this._settings = {}; //settings for the speedtest worker
|
||||
this._state = 0; //0=adding settings, 1=adding servers, 2=server selection done, 3=test running, 4=done
|
||||
}
|
||||
|
||||
Speedtest.prototype = {
|
||||
constructor: Speedtest,
|
||||
/**
|
||||
* Returns the state of the test: 0=adding settings, 1=adding servers, 2=server selection done, 3=test running, 4=done
|
||||
*/
|
||||
getState: function() {
|
||||
return this._state;
|
||||
},
|
||||
/**
|
||||
* Change one of the test settings from their defaults.
|
||||
* - parameter: string with the name of the parameter that you want to set
|
||||
* - value: new value for the parameter
|
||||
*
|
||||
* Invalid values or nonexistant parameters will be ignored by the speedtest worker.
|
||||
*/
|
||||
setParameter: function(parameter, value) {
|
||||
if (this._state == 3)
|
||||
throw "You cannot change the test settings while running the test";
|
||||
this._settings[parameter] = value;
|
||||
if(parameter === "telemetry_extra"){
|
||||
this._originalExtra=this._settings.telemetry_extra;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Used internally to check if a server object contains all the required elements.
|
||||
* Also fixes the server URL if needed.
|
||||
*/
|
||||
_checkServerDefinition: function(server) {
|
||||
try {
|
||||
if (typeof server.name !== "string")
|
||||
throw "Name string missing from server definition (name)";
|
||||
if (typeof server.server !== "string")
|
||||
throw "Server address string missing from server definition (server)";
|
||||
if (server.server.charAt(server.server.length - 1) != "/")
|
||||
server.server += "/";
|
||||
if (server.server.indexOf("//") == 0)
|
||||
server.server = location.protocol + server.server;
|
||||
if (typeof server.dlURL !== "string")
|
||||
throw "Download URL string missing from server definition (dlURL)";
|
||||
if (typeof server.ulURL !== "string")
|
||||
throw "Upload URL string missing from server definition (ulURL)";
|
||||
if (typeof server.pingURL !== "string")
|
||||
throw "Ping URL string missing from server definition (pingURL)";
|
||||
if (typeof server.getIpURL !== "string")
|
||||
throw "GetIP URL string missing from server definition (getIpURL)";
|
||||
} catch (e) {
|
||||
throw "Invalid server definition";
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Add a test point (multiple points of test)
|
||||
* server: the server to be added as an object. Defined in app/js/speedtestUI.js
|
||||
*/
|
||||
addTestPoint: function(server) {
|
||||
this._checkServerDefinition(server);
|
||||
if (this._state == 0) this._state = 1;
|
||||
if (this._state != 1) throw "You can't add a server after server selection";
|
||||
this._settings.mpot = true;
|
||||
this._serverList.push(server);
|
||||
},
|
||||
/**
|
||||
* Same as addTestPoint, but you can pass an array of servers
|
||||
*/
|
||||
addTestPoints: function(list) {
|
||||
for (var i = 0; i < list.length; i++) this.addTestPoint(list[i]);
|
||||
},
|
||||
/**
|
||||
* Load a JSON server list from URL (multiple points of test)
|
||||
* url: the url where the server list can be fetched. Must be an array with objects containing the following elements:
|
||||
* result: callback to be called when the list is loaded correctly. An array with the loaded servers will be passed to this function, or null if it failed
|
||||
*/
|
||||
loadServerList: function(url,result) {
|
||||
if (this._state == 0) this._state = 1;
|
||||
if (this._state != 1) throw "You can't add a server after server selection";
|
||||
this._settings.mpot = true;
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onload = function(){
|
||||
try{
|
||||
var servers=JSON.parse(xhr.responseText);
|
||||
for(var i=0;i<servers.length;i++){
|
||||
this._checkServerDefinition(servers[i]);
|
||||
}
|
||||
this.addTestPoints(servers);
|
||||
result(servers);
|
||||
}catch(e){
|
||||
result(null);
|
||||
}
|
||||
}.bind(this);
|
||||
xhr.onerror = function(){result(null);}
|
||||
xhr.open("GET",url);
|
||||
xhr.send();
|
||||
},
|
||||
/**
|
||||
* Returns the selected server (multiple points of test)
|
||||
*/
|
||||
getSelectedServer: function() {
|
||||
if (this._state < 2 || this._selectedServer == null)
|
||||
throw "No server is selected";
|
||||
return this._selectedServer;
|
||||
},
|
||||
/**
|
||||
* Manually selects one of the test points (multiple points of test)
|
||||
*/
|
||||
setSelectedServer: function(server) {
|
||||
this._checkServerDefinition(server);
|
||||
if (this._state == 3)
|
||||
throw "You can't select a server while the test is running";
|
||||
this._selectedServer = server;
|
||||
this._state = 2;
|
||||
},
|
||||
/**
|
||||
* Automatically selects a server from the list of added test points. The server with the lowest ping will be chosen. (multiple points of test)
|
||||
* The process is asynchronous and the passed result callback function will be called when it's done, then the test can be started.
|
||||
*/
|
||||
selectServer: function(result) {
|
||||
if (this._state != 1) {
|
||||
if (this._state == 0) throw "No test points added";
|
||||
if (this._state == 2) throw "Server already selected";
|
||||
if (this._state >= 3)
|
||||
throw "You can't select a server while the test is running";
|
||||
}
|
||||
if (this._selectServerCalled) throw "selectServer already called"; else this._selectServerCalled=true;
|
||||
/*this function goes through a list of servers. For each server, the ping is measured, then the server with the function selected is called with the best server, or null if all the servers were down.
|
||||
*/
|
||||
var select = function(serverList, selected) {
|
||||
//pings the specified URL, then calls the function result. Result will receive a parameter which is either the time it took to ping the URL, or -1 if something went wrong.
|
||||
var PING_TIMEOUT = 2000;
|
||||
var USE_PING_TIMEOUT = true; //will be disabled on unsupported browsers
|
||||
if (/MSIE.(\d+\.\d+)/i.test(navigator.userAgent)) {
|
||||
//IE11 doesn't support XHR timeout
|
||||
USE_PING_TIMEOUT = false;
|
||||
}
|
||||
var ping = function(url, rtt) {
|
||||
url += (url.match(/\?/) ? "&" : "?") + "cors=true";
|
||||
var xhr = new XMLHttpRequest();
|
||||
var t = new Date().getTime();
|
||||
xhr.onload = function() {
|
||||
if (xhr.responseText.length == 0) {
|
||||
//we expect an empty response
|
||||
var instspd = new Date().getTime() - t; //rough timing estimate
|
||||
try {
|
||||
//try to get more accurate timing using performance API
|
||||
var p = performance.getEntriesByName(url);
|
||||
p = p[p.length - 1];
|
||||
var d = p.responseStart - p.requestStart;
|
||||
if (d <= 0) d = p.duration;
|
||||
if (d > 0 && d < instspd) instspd = d;
|
||||
} catch (e) {}
|
||||
rtt(instspd);
|
||||
} else rtt(-1);
|
||||
}.bind(this);
|
||||
xhr.onerror = function() {
|
||||
rtt(-1);
|
||||
}.bind(this);
|
||||
xhr.open("GET", url);
|
||||
if (USE_PING_TIMEOUT) {
|
||||
try {
|
||||
xhr.timeout = PING_TIMEOUT;
|
||||
xhr.ontimeout = xhr.onerror;
|
||||
} catch (e) {}
|
||||
}
|
||||
xhr.send();
|
||||
}.bind(this);
|
||||
|
||||
/**
|
||||
* This function repeatedly pings a server to get a good estimate of the ping. When it's done, it calls the done function without parameters.
|
||||
* At the end of the execution, the server will have a new parameter called pingT, which is either the best ping we got from the server or -1 if something went wrong.
|
||||
*/
|
||||
var PINGS = 3, //up to 3 pings are performed, unless the server is down...
|
||||
SLOW_THRESHOLD = 500; //...or one of the pings is above this threshold
|
||||
var checkServer = function(server, done) {
|
||||
var i = 0;
|
||||
server.pingT = -1;
|
||||
if (server.server.indexOf(location.protocol) == -1) done();
|
||||
else {
|
||||
var nextPing = function() {
|
||||
if (i++ == PINGS) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
ping(
|
||||
server.server + server.pingURL,
|
||||
function(t) {
|
||||
if (t >= 0) {
|
||||
if (t < server.pingT || server.pingT == -1) server.pingT = t;
|
||||
if (t < SLOW_THRESHOLD) nextPing();
|
||||
else done();
|
||||
} else done();
|
||||
}.bind(this)
|
||||
);
|
||||
}.bind(this);
|
||||
nextPing();
|
||||
}
|
||||
}.bind(this);
|
||||
//check servers in list, one by one
|
||||
var i = 0;
|
||||
var done = function() {
|
||||
var bestServer = null;
|
||||
for (var i = 0; i < serverList.length; i++) {
|
||||
if (
|
||||
serverList[i].pingT != -1 &&
|
||||
(bestServer == null || serverList[i].pingT < bestServer.pingT)
|
||||
)
|
||||
bestServer = serverList[i];
|
||||
}
|
||||
selected(bestServer);
|
||||
}.bind(this);
|
||||
var nextServer = function() {
|
||||
if (i == serverList.length) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
checkServer(serverList[i++], nextServer);
|
||||
}.bind(this);
|
||||
nextServer();
|
||||
}.bind(this);
|
||||
|
||||
//parallel server selection
|
||||
var CONCURRENCY = 6;
|
||||
var serverLists = [];
|
||||
for (var i = 0; i < CONCURRENCY; i++) {
|
||||
serverLists[i] = [];
|
||||
}
|
||||
for (var i = 0; i < this._serverList.length; i++) {
|
||||
serverLists[i % CONCURRENCY].push(this._serverList[i]);
|
||||
}
|
||||
var completed = 0;
|
||||
var bestServer = null;
|
||||
for (var i = 0; i < CONCURRENCY; i++) {
|
||||
select(
|
||||
serverLists[i],
|
||||
function(server) {
|
||||
if (server != null) {
|
||||
if (bestServer == null || server.pingT < bestServer.pingT)
|
||||
bestServer = server;
|
||||
}
|
||||
completed++;
|
||||
if (completed == CONCURRENCY) {
|
||||
this._selectedServer = bestServer;
|
||||
this._state = 2;
|
||||
if (result) result(bestServer);
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Starts the test.
|
||||
* During the test, the onupdate(data) callback function will be called periodically with data from the worker.
|
||||
* At the end of the test, the onend(aborted) function will be called with a boolean telling you if the test was aborted or if it ended normally.
|
||||
*/
|
||||
start: function() {
|
||||
if (this._state == 3) throw "Test already running";
|
||||
this.worker = new Worker("dist/speedtest/speedtest_worker.js?r=" + Math.random());
|
||||
this.worker.onmessage = function(e) {
|
||||
if (e.data === this._prevData) return;
|
||||
else this._prevData = e.data;
|
||||
var data = JSON.parse(e.data);
|
||||
try {
|
||||
if (this.onupdate) this.onupdate(data);
|
||||
} catch (e) {
|
||||
console.error("Speedtest onupdate event threw exception: " + e);
|
||||
}
|
||||
if (data.testState >= 4) {
|
||||
clearInterval(this.updater);
|
||||
this._state = 4;
|
||||
try {
|
||||
if (this.onend) this.onend(data.testState == 5);
|
||||
} catch (e) {
|
||||
console.error("Speedtest onend event threw exception: " + e);
|
||||
}
|
||||
}
|
||||
}.bind(this);
|
||||
this.updater = setInterval(
|
||||
function() {
|
||||
this.worker.postMessage("status");
|
||||
}.bind(this),
|
||||
200
|
||||
);
|
||||
|
||||
if (this._state == 1)
|
||||
throw "When using multiple points of test, you must call selectServer before starting the test";
|
||||
if (this._state == 2) {
|
||||
this._settings.url_dl =
|
||||
this._selectedServer.server + this._selectedServer.dlURL;
|
||||
this._settings.url_ul =
|
||||
this._selectedServer.server + this._selectedServer.ulURL;
|
||||
this._settings.url_ping =
|
||||
this._selectedServer.server + this._selectedServer.pingURL;
|
||||
this._settings.url_getIp =
|
||||
this._selectedServer.server + this._selectedServer.getIpURL;
|
||||
if (typeof this._originalExtra !== "undefined") {
|
||||
this._settings.telemetry_extra = JSON.stringify({
|
||||
server: this._selectedServer.name,
|
||||
extra: this._originalExtra
|
||||
});
|
||||
} else
|
||||
this._settings.telemetry_extra = JSON.stringify({
|
||||
server: this._selectedServer.name
|
||||
});
|
||||
}
|
||||
this._state = 3;
|
||||
this.worker.postMessage("start " + JSON.stringify(this._settings));
|
||||
},
|
||||
/**
|
||||
* Aborts the test while it's running.
|
||||
*/
|
||||
abort: function() {
|
||||
if (this._state < 3) throw "You cannot abort a test that's not started yet";
|
||||
if (this._state < 4) this.worker.postMessage("abort");
|
||||
}
|
||||
};
|
||||
|
||||
725
dist/speedtest/speedtest_worker.js
vendored
Executable file
725
dist/speedtest/speedtest_worker.js
vendored
Executable file
@@ -0,0 +1,725 @@
|
||||
/*
|
||||
LibreSpeed - Worker
|
||||
by Federico Dossena
|
||||
https://github.com/librespeed/speedtest/
|
||||
GNU LGPLv3 License
|
||||
*/
|
||||
|
||||
// data reported to main thread
|
||||
var testState = -1; // -1=not started, 0=starting, 1=download test, 2=ping+jitter test, 3=upload test, 4=finished, 5=abort
|
||||
var dlStatus = ""; // download speed in megabit/s with 2 decimal digits
|
||||
var ulStatus = ""; // upload speed in megabit/s with 2 decimal digits
|
||||
var pingStatus = ""; // ping in milliseconds with 2 decimal digits
|
||||
var jitterStatus = ""; // jitter in milliseconds with 2 decimal digits
|
||||
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 = null; //test ID (sent back by telemetry if used, null otherwise)
|
||||
|
||||
var log = ""; //telemetry log
|
||||
function tlog(s) {
|
||||
if (settings.telemetry_level >= 2) {
|
||||
log += Date.now() + ": " + s + "\n";
|
||||
}
|
||||
}
|
||||
function tverb(s) {
|
||||
if (settings.telemetry_level >= 3) {
|
||||
log += Date.now() + ": " + s + "\n";
|
||||
}
|
||||
}
|
||||
function twarn(s) {
|
||||
if (settings.telemetry_level >= 2) {
|
||||
log += Date.now() + " WARN: " + s + "\n";
|
||||
}
|
||||
console.warn(s);
|
||||
}
|
||||
|
||||
// test settings. can be overridden by sending specific values with the start command
|
||||
var settings = {
|
||||
mpot: true, //set to true when in MPOT mode
|
||||
test_order: "IP_D_U", //order in which tests will be performed as a string. D=Download, U=Upload, P=Ping+Jitter, I=IP, _=1 second delay
|
||||
time_ul_max: 15, // max duration of upload test in seconds
|
||||
time_dl_max: 15, // max duration of download test in seconds
|
||||
time_auto: true, // if set to true, tests will take less time on faster connections
|
||||
time_ulGraceTime: 3, //time to wait in seconds before actually measuring ul speed (wait for buffers to fill)
|
||||
time_dlGraceTime: 1.5, //time to wait in seconds before actually measuring dl speed (wait for TCP window to increase)
|
||||
count_ping: 10, // number of pings to perform in ping test
|
||||
url_dl: "backend/garbage.php", // path to a large file or garbage.php, used for download test. must be relative to this js file
|
||||
url_ul: "backend/empty.php", // path to an empty file, used for upload test. must be relative to this js file
|
||||
url_ping: "backend/empty.php", // path to an empty file, used for ping test. must be relative to this js file
|
||||
url_getIp: "backend/getIP.php", // path to getIP.php relative to this js file, or a similar thing that outputs the client's ip
|
||||
getIp_ispInfo: true, //if set to true, the server will include ISP info with the IP address
|
||||
getIp_ispInfo_distance: "km", //km or mi=estimate distance from server in km/mi; set to false to disable distance estimation. getIp_ispInfo must be enabled in order for this to work
|
||||
xhr_dlMultistream: 6, // number of download streams to use (can be different if enable_quirks is active)
|
||||
xhr_ulMultistream: 3, // number of upload streams to use (can be different if enable_quirks is active)
|
||||
xhr_multistreamDelay: 300, //how much concurrent requests should be delayed
|
||||
xhr_ignoreErrors: 1, // 0=fail on errors, 1=attempt to restart a stream if it fails, 2=ignore all errors
|
||||
xhr_dlUseBlob: false, // if set to true, it reduces ram usage but uses the hard drive (useful with large garbagePhp_chunkSize and/or high xhr_dlMultistream)
|
||||
xhr_ul_blob_megabytes: 20, //size in megabytes of the upload blobs sent in the upload test (forced to 4 on chrome mobile)
|
||||
garbagePhp_chunkSize: 100, // size of chunks sent by garbage.php (can be different if enable_quirks is active)
|
||||
enable_quirks: true, // enable quirks for specific browsers. currently it overrides settings to optimize for specific browsers, unless they are already being overridden with the start command
|
||||
ping_allowPerformanceApi: true, // if enabled, the ping test will attempt to calculate the ping more precisely using the Performance API. Currently works perfectly in Chrome, badly in Edge, and not at all in Firefox. If Performance API is not supported or the result is obviously wrong, a fallback is provided.
|
||||
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: 1, // 0=disabled, 1=basic (results only), 2=full (results and timing) 3=debug (results+log)
|
||||
url_telemetry: "https://speedtest.raspap.com/results/telemetry.php", // path to the script that adds telemetry data to the database
|
||||
telemetry_extra: "", //extra data that can be passed to the telemetry through the settings
|
||||
forceIE11Workaround: false //when set to true, it will foce the IE11 upload test on all browsers. Debug only
|
||||
};
|
||||
|
||||
var xhr = null; // array of currently active xhr requests
|
||||
var interval = null; // timer used in tests
|
||||
var test_pointer = 0; //pointer to the next test to run inside settings.test_order
|
||||
|
||||
/*
|
||||
this function is used on URLs passed in the settings to determine whether we need a ? or an & as a separator
|
||||
*/
|
||||
function url_sep(url) {
|
||||
return url.match(/\?/) ? "&" : "?";
|
||||
}
|
||||
|
||||
/*
|
||||
listener for commands from main thread to this worker.
|
||||
commands:
|
||||
-status: returns the current status as a JSON string containing testState, dlStatus, ulStatus, pingStatus, clientIp, jitterStatus, dlProgress, ulProgress, pingProgress
|
||||
-abort: aborts the current test
|
||||
-start: starts the test. optionally, settings can be passed as JSON.
|
||||
example: start {"time_ul_max":"10", "time_dl_max":"10", "count_ping":"50"}
|
||||
*/
|
||||
this.addEventListener("message", function(e) {
|
||||
var params = e.data.split(" ");
|
||||
if (params[0] === "status") {
|
||||
// return status
|
||||
postMessage(
|
||||
JSON.stringify({
|
||||
testState: testState,
|
||||
dlStatus: dlStatus,
|
||||
ulStatus: ulStatus,
|
||||
pingStatus: pingStatus,
|
||||
clientIp: clientIp,
|
||||
jitterStatus: jitterStatus,
|
||||
dlProgress: dlProgress,
|
||||
ulProgress: ulProgress,
|
||||
pingProgress: pingProgress,
|
||||
testId: testId
|
||||
})
|
||||
);
|
||||
}
|
||||
if (params[0] === "start" && testState === -1) {
|
||||
// start new test
|
||||
testState = 0;
|
||||
try {
|
||||
// parse settings, if present
|
||||
var s = {};
|
||||
try {
|
||||
var ss = e.data.substring(5);
|
||||
if (ss) s = JSON.parse(ss);
|
||||
} catch (e) {
|
||||
twarn("Error parsing custom settings JSON. Please check your syntax");
|
||||
}
|
||||
//copy custom settings
|
||||
for (var key in s) {
|
||||
if (typeof settings[key] !== "undefined") settings[key] = s[key];
|
||||
else twarn("Unknown setting ignored: " + key);
|
||||
}
|
||||
var ua = navigator.userAgent;
|
||||
// quirks for specific browsers. apply only if not overridden. more may be added in future releases
|
||||
if (settings.enable_quirks || (typeof s.enable_quirks !== "undefined" && s.enable_quirks)) {
|
||||
if (/Firefox.(\d+\.\d+)/i.test(ua)) {
|
||||
if (typeof s.ping_allowPerformanceApi === "undefined") {
|
||||
// ff performance API sucks
|
||||
settings.ping_allowPerformanceApi = false;
|
||||
}
|
||||
}
|
||||
if (/Edge.(\d+\.\d+)/i.test(ua)) {
|
||||
if (typeof s.xhr_dlMultistream === "undefined") {
|
||||
// edge more precise with 3 download streams
|
||||
settings.xhr_dlMultistream = 3;
|
||||
}
|
||||
}
|
||||
if (/Chrome.(\d+)/i.test(ua) && !!self.fetch) {
|
||||
if (typeof s.xhr_dlMultistream === "undefined") {
|
||||
// chrome more precise with 5 streams
|
||||
settings.xhr_dlMultistream = 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (/Edge.(\d+\.\d+)/i.test(ua)) {
|
||||
//Edge 15 introduced a bug that causes onprogress events to not get fired, we have to use the "small chunks" workaround that reduces accuracy
|
||||
settings.forceIE11Workaround = true;
|
||||
}
|
||||
if (/PlayStation 4.(\d+\.\d+)/i.test(ua)) {
|
||||
//PS4 browser has the same bug as IE11/Edge
|
||||
settings.forceIE11Workaround = true;
|
||||
}
|
||||
if (/Chrome.(\d+)/i.test(ua) && /Android|iPhone|iPad|iPod|Windows Phone/i.test(ua)) {
|
||||
//cheap af
|
||||
//Chrome mobile introduced a limitation somewhere around version 65, we have to limit XHR upload size to 4 megabytes
|
||||
settings.xhr_ul_blob_megabytes = 4;
|
||||
}
|
||||
if (/^((?!chrome|android|crios|fxios).)*safari/i.test(ua)) {
|
||||
//Safari also needs the IE11 workaround but only for the MPOT version
|
||||
settings.forceIE11Workaround = true;
|
||||
}
|
||||
//telemetry_level has to be parsed and not just copied
|
||||
if (typeof s.telemetry_level !== "undefined") settings.telemetry_level = s.telemetry_level === "basic" ? 1 : s.telemetry_level === "full" ? 2 : s.telemetry_level === "debug" ? 3 : 0; // telemetry level
|
||||
//transform test_order to uppercase, just in case
|
||||
settings.test_order = settings.test_order.toUpperCase();
|
||||
} catch (e) {
|
||||
twarn("Possible error in custom test settings. Some settings might not have been applied. Exception: " + e);
|
||||
}
|
||||
// run the tests
|
||||
tverb(JSON.stringify(settings));
|
||||
test_pointer = 0;
|
||||
var iRun = false,
|
||||
dRun = false,
|
||||
uRun = false,
|
||||
pRun = false;
|
||||
var runNextTest = function() {
|
||||
if (testState == 5) return;
|
||||
if (test_pointer >= settings.test_order.length) {
|
||||
//test is finished
|
||||
if (settings.telemetry_level > 0)
|
||||
sendTelemetry(function(id) {
|
||||
testState = 4;
|
||||
if (id != null) testId = id;
|
||||
});
|
||||
else testState = 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;
|
||||
testState = 1;
|
||||
dlTest(runNextTest);
|
||||
}
|
||||
break;
|
||||
case "U":
|
||||
{
|
||||
test_pointer++;
|
||||
if (uRun) {
|
||||
runNextTest();
|
||||
return;
|
||||
} else uRun = true;
|
||||
testState = 3;
|
||||
ulTest(runNextTest);
|
||||
}
|
||||
break;
|
||||
case "P":
|
||||
{
|
||||
test_pointer++;
|
||||
if (pRun) {
|
||||
runNextTest();
|
||||
return;
|
||||
} else pRun = true;
|
||||
testState = 2;
|
||||
pingTest(runNextTest);
|
||||
}
|
||||
break;
|
||||
case "_":
|
||||
{
|
||||
test_pointer++;
|
||||
setTimeout(runNextTest, 1000);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
test_pointer++;
|
||||
}
|
||||
};
|
||||
runNextTest();
|
||||
}
|
||||
if (params[0] === "abort") {
|
||||
// abort command
|
||||
if (testState >= 4) return;
|
||||
tlog("manually aborted");
|
||||
clearRequests(); // stop all xhr activity
|
||||
runNextTest = null;
|
||||
if (interval) clearInterval(interval); // clear timer if present
|
||||
if (settings.telemetry_level > 1) sendTelemetry(function() {});
|
||||
testState = 5; //set test as aborted
|
||||
dlStatus = "";
|
||||
ulStatus = "";
|
||||
pingStatus = "";
|
||||
jitterStatus = "";
|
||||
clientIp = "";
|
||||
dlProgress = 0;
|
||||
ulProgress = 0;
|
||||
pingProgress = 0;
|
||||
}
|
||||
});
|
||||
// stops all XHR activity, aggressively
|
||||
function clearRequests() {
|
||||
tverb("stopping pending XHRs");
|
||||
if (xhr) {
|
||||
for (var i = 0; i < xhr.length; i++) {
|
||||
try {
|
||||
xhr[i].onprogress = null;
|
||||
xhr[i].onload = null;
|
||||
xhr[i].onerror = null;
|
||||
} catch (e) {}
|
||||
try {
|
||||
xhr[i].upload.onprogress = null;
|
||||
xhr[i].upload.onload = null;
|
||||
xhr[i].upload.onerror = null;
|
||||
} catch (e) {}
|
||||
try {
|
||||
xhr[i].abort();
|
||||
} catch (e) {}
|
||||
try {
|
||||
delete xhr[i];
|
||||
} catch (e) {}
|
||||
}
|
||||
xhr = null;
|
||||
}
|
||||
}
|
||||
// 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) {
|
||||
tverb("getIp");
|
||||
if (ipCalled) return;
|
||||
else ipCalled = true; // getIp already called?
|
||||
var startT = new Date().getTime();
|
||||
xhr = new XMLHttpRequest();
|
||||
xhr.onload = function() {
|
||||
tlog("IP: " + xhr.responseText + ", took " + (new Date().getTime() - startT) + "ms");
|
||||
try {
|
||||
var data = JSON.parse(xhr.responseText);
|
||||
clientIp = data.processedString;
|
||||
ispInfo = data.rawIspInfo;
|
||||
} catch (e) {
|
||||
clientIp = xhr.responseText;
|
||||
ispInfo = "";
|
||||
}
|
||||
done();
|
||||
};
|
||||
xhr.onerror = function() {
|
||||
tlog("getIp failed, took " + (new Date().getTime() - startT) + "ms");
|
||||
done();
|
||||
};
|
||||
xhr.open("GET", settings.url_getIp + url_sep(settings.url_getIp) + (settings.mpot ? "cors=true&" : "") + (settings.getIp_ispInfo ? "isp=true" + (settings.getIp_ispInfo_distance ? "&distance=" + settings.getIp_ispInfo_distance + "&" : "&") : "&") + "r=" + Math.random(), true);
|
||||
xhr.send();
|
||||
}
|
||||
// download test, calls done function when it's over
|
||||
var dlCalled = false; // used to prevent multiple accidental calls to dlTest
|
||||
function dlTest(done) {
|
||||
tverb("dlTest");
|
||||
if (dlCalled) return;
|
||||
else dlCalled = true; // dlTest already called?
|
||||
var totLoaded = 0.0, // total number of loaded bytes
|
||||
startT = new Date().getTime(), // timestamp when test was started
|
||||
bonusT = 0, //how many milliseconds the test has been shortened by (higher on faster connections)
|
||||
graceTimeDone = false, //set to true after the grace time is past
|
||||
failed = false; // set to true if a stream fails
|
||||
xhr = [];
|
||||
// function to create a download stream. streams are slightly delayed so that they will not end at the same time
|
||||
var testStream = function(i, delay) {
|
||||
setTimeout(
|
||||
function() {
|
||||
if (testState !== 1) return; // delayed stream ended up starting after the end of the download test
|
||||
tverb("dl test stream started " + i + " " + delay);
|
||||
var prevLoaded = 0; // number of bytes loaded last time onprogress was called
|
||||
var x = new XMLHttpRequest();
|
||||
xhr[i] = x;
|
||||
xhr[i].onprogress = function(event) {
|
||||
tverb("dl stream progress event " + i + " " + event.loaded);
|
||||
if (testState !== 1) {
|
||||
try {
|
||||
x.abort();
|
||||
} catch (e) {}
|
||||
} // just in case this XHR is still running after the download test
|
||||
// progress event, add number of new loaded bytes to totLoaded
|
||||
var loadDiff = event.loaded <= 0 ? 0 : event.loaded - prevLoaded;
|
||||
if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return; // just in case
|
||||
totLoaded += loadDiff;
|
||||
prevLoaded = event.loaded;
|
||||
}.bind(this);
|
||||
xhr[i].onload = function() {
|
||||
// the large file has been loaded entirely, start again
|
||||
tverb("dl stream finished " + i);
|
||||
try {
|
||||
xhr[i].abort();
|
||||
} catch (e) {} // reset the stream data to empty ram
|
||||
testStream(i, 0);
|
||||
}.bind(this);
|
||||
xhr[i].onerror = function() {
|
||||
// error
|
||||
tverb("dl stream failed " + i);
|
||||
if (settings.xhr_ignoreErrors === 0) failed = true; //abort
|
||||
try {
|
||||
xhr[i].abort();
|
||||
} catch (e) {}
|
||||
delete xhr[i];
|
||||
if (settings.xhr_ignoreErrors === 1) testStream(i, 0); //restart stream
|
||||
}.bind(this);
|
||||
// send xhr
|
||||
try {
|
||||
if (settings.xhr_dlUseBlob) xhr[i].responseType = "blob";
|
||||
else xhr[i].responseType = "arraybuffer";
|
||||
} catch (e) {}
|
||||
xhr[i].open("GET", settings.url_dl + url_sep(settings.url_dl) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random() + "&ckSize=" + settings.garbagePhp_chunkSize, true); // random string to prevent caching
|
||||
xhr[i].send();
|
||||
}.bind(this),
|
||||
1 + delay
|
||||
);
|
||||
}.bind(this);
|
||||
// open streams
|
||||
for (var i = 0; i < settings.xhr_dlMultistream; i++) {
|
||||
testStream(i, settings.xhr_multistreamDelay * i);
|
||||
}
|
||||
// every 200ms, update dlStatus
|
||||
interval = setInterval(
|
||||
function() {
|
||||
tverb("DL: " + dlStatus + (graceTimeDone ? "" : " (in grace time)"));
|
||||
var t = new Date().getTime() - startT;
|
||||
if (graceTimeDone) dlProgress = (t + bonusT) / (settings.time_dl_max * 1000);
|
||||
if (t < 200) return;
|
||||
if (!graceTimeDone) {
|
||||
if (t > 1000 * settings.time_dlGraceTime) {
|
||||
if (totLoaded > 0) {
|
||||
// if the connection is so slow that we didn't get a single chunk yet, do not reset
|
||||
startT = new Date().getTime();
|
||||
bonusT = 0;
|
||||
totLoaded = 0.0;
|
||||
}
|
||||
graceTimeDone = true;
|
||||
}
|
||||
} else {
|
||||
var speed = totLoaded / (t / 1000.0);
|
||||
if (settings.time_auto) {
|
||||
//decide how much to shorten the test. Every 200ms, the test is shortened by the bonusT calculated here
|
||||
var bonus = (5.0 * speed) / 100000;
|
||||
bonusT += bonus > 400 ? 400 : bonus;
|
||||
}
|
||||
//update status
|
||||
dlStatus = ((speed * 8 * settings.overheadCompensationFactor) / (settings.useMebibits ? 1048576 : 1000000)).toFixed(2); // speed is multiplied by 8 to go from bytes to bits, overhead compensation is applied, then everything is divided by 1048576 or 1000000 to go to megabits/mebibits
|
||||
if ((t + bonusT) / 1000.0 > settings.time_dl_max || failed) {
|
||||
// test is over, stop streams and timer
|
||||
if (failed || isNaN(dlStatus)) dlStatus = "Fail";
|
||||
clearRequests();
|
||||
clearInterval(interval);
|
||||
dlProgress = 1;
|
||||
tlog("dlTest: " + dlStatus + ", took " + (new Date().getTime() - startT) + "ms");
|
||||
done();
|
||||
}
|
||||
}
|
||||
}.bind(this),
|
||||
200
|
||||
);
|
||||
}
|
||||
// upload test, calls done function whent it's over
|
||||
var ulCalled = false; // used to prevent multiple accidental calls to ulTest
|
||||
function ulTest(done) {
|
||||
tverb("ulTest");
|
||||
if (ulCalled) return;
|
||||
else ulCalled = true; // ulTest already called?
|
||||
// garbage data for upload test
|
||||
var r = new ArrayBuffer(1048576);
|
||||
var maxInt = Math.pow(2, 32) - 1;
|
||||
try {
|
||||
r = new Uint32Array(r);
|
||||
for (var i = 0; i < r.length; i++) r[i] = Math.random() * maxInt;
|
||||
} catch (e) {}
|
||||
var req = [];
|
||||
var reqsmall = [];
|
||||
for (var i = 0; i < settings.xhr_ul_blob_megabytes; i++) req.push(r);
|
||||
req = new Blob(req);
|
||||
r = new ArrayBuffer(262144);
|
||||
try {
|
||||
r = new Uint32Array(r);
|
||||
for (var i = 0; i < r.length; i++) r[i] = Math.random() * maxInt;
|
||||
} catch (e) {}
|
||||
reqsmall.push(r);
|
||||
reqsmall = new Blob(reqsmall);
|
||||
var testFunction = function() {
|
||||
var totLoaded = 0.0, // total number of transmitted bytes
|
||||
startT = new Date().getTime(), // timestamp when test was started
|
||||
bonusT = 0, //how many milliseconds the test has been shortened by (higher on faster connections)
|
||||
graceTimeDone = false, //set to true after the grace time is past
|
||||
failed = false; // set to true if a stream fails
|
||||
xhr = [];
|
||||
// function to create an upload stream. streams are slightly delayed so that they will not end at the same time
|
||||
var testStream = function(i, delay) {
|
||||
setTimeout(
|
||||
function() {
|
||||
if (testState !== 3) return; // delayed stream ended up starting after the end of the upload test
|
||||
tverb("ul test stream started " + i + " " + delay);
|
||||
var prevLoaded = 0; // number of bytes transmitted last time onprogress was called
|
||||
var x = new XMLHttpRequest();
|
||||
xhr[i] = x;
|
||||
var ie11workaround;
|
||||
if (settings.forceIE11Workaround) ie11workaround = true;
|
||||
else {
|
||||
try {
|
||||
xhr[i].upload.onprogress;
|
||||
ie11workaround = false;
|
||||
} catch (e) {
|
||||
ie11workaround = true;
|
||||
}
|
||||
}
|
||||
if (ie11workaround) {
|
||||
// IE11 workarond: xhr.upload does not work properly, therefore we send a bunch of small 256k requests and use the onload event as progress. This is not precise, especially on fast connections
|
||||
xhr[i].onload = xhr[i].onerror = function() {
|
||||
tverb("ul stream progress event (ie11wa)");
|
||||
totLoaded += reqsmall.size;
|
||||
testStream(i, 0);
|
||||
};
|
||||
xhr[i].open("POST", settings.url_ul + url_sep(settings.url_ul) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); // random string to prevent caching
|
||||
try {
|
||||
xhr[i].setRequestHeader("Content-Encoding", "identity"); // disable compression (some browsers may refuse it, but data is incompressible anyway)
|
||||
} catch (e) {}
|
||||
//No Content-Type header in MPOT branch because it triggers bugs in some browsers
|
||||
xhr[i].send(reqsmall);
|
||||
} else {
|
||||
// REGULAR version, no workaround
|
||||
xhr[i].upload.onprogress = function(event) {
|
||||
tverb("ul stream progress event " + i + " " + event.loaded);
|
||||
if (testState !== 3) {
|
||||
try {
|
||||
x.abort();
|
||||
} catch (e) {}
|
||||
} // just in case this XHR is still running after the upload test
|
||||
// progress event, add number of new loaded bytes to totLoaded
|
||||
var loadDiff = event.loaded <= 0 ? 0 : event.loaded - prevLoaded;
|
||||
if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return; // just in case
|
||||
totLoaded += loadDiff;
|
||||
prevLoaded = event.loaded;
|
||||
}.bind(this);
|
||||
xhr[i].upload.onload = function() {
|
||||
// this stream sent all the garbage data, start again
|
||||
tverb("ul stream finished " + i);
|
||||
testStream(i, 0);
|
||||
}.bind(this);
|
||||
xhr[i].upload.onerror = function() {
|
||||
tverb("ul stream failed " + i);
|
||||
if (settings.xhr_ignoreErrors === 0) failed = true; //abort
|
||||
try {
|
||||
xhr[i].abort();
|
||||
} catch (e) {}
|
||||
delete xhr[i];
|
||||
if (settings.xhr_ignoreErrors === 1) testStream(i, 0); //restart stream
|
||||
}.bind(this);
|
||||
// send xhr
|
||||
xhr[i].open("POST", settings.url_ul + url_sep(settings.url_ul) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); // random string to prevent caching
|
||||
try {
|
||||
xhr[i].setRequestHeader("Content-Encoding", "identity"); // disable compression (some browsers may refuse it, but data is incompressible anyway)
|
||||
} catch (e) {}
|
||||
//No Content-Type header in MPOT branch because it triggers bugs in some browsers
|
||||
xhr[i].send(req);
|
||||
}
|
||||
}.bind(this),
|
||||
delay
|
||||
);
|
||||
}.bind(this);
|
||||
// open streams
|
||||
for (var i = 0; i < settings.xhr_ulMultistream; i++) {
|
||||
testStream(i, settings.xhr_multistreamDelay * i);
|
||||
}
|
||||
// every 200ms, update ulStatus
|
||||
interval = setInterval(
|
||||
function() {
|
||||
tverb("UL: " + ulStatus + (graceTimeDone ? "" : " (in grace time)"));
|
||||
var t = new Date().getTime() - startT;
|
||||
if (graceTimeDone) ulProgress = (t + bonusT) / (settings.time_ul_max * 1000);
|
||||
if (t < 200) return;
|
||||
if (!graceTimeDone) {
|
||||
if (t > 1000 * settings.time_ulGraceTime) {
|
||||
if (totLoaded > 0) {
|
||||
// if the connection is so slow that we didn't get a single chunk yet, do not reset
|
||||
startT = new Date().getTime();
|
||||
bonusT = 0;
|
||||
totLoaded = 0.0;
|
||||
}
|
||||
graceTimeDone = true;
|
||||
}
|
||||
} else {
|
||||
var speed = totLoaded / (t / 1000.0);
|
||||
if (settings.time_auto) {
|
||||
//decide how much to shorten the test. Every 200ms, the test is shortened by the bonusT calculated here
|
||||
var bonus = (5.0 * speed) / 100000;
|
||||
bonusT += bonus > 400 ? 400 : bonus;
|
||||
}
|
||||
//update status
|
||||
ulStatus = ((speed * 8 * settings.overheadCompensationFactor) / (settings.useMebibits ? 1048576 : 1000000)).toFixed(2); // speed is multiplied by 8 to go from bytes to bits, overhead compensation is applied, then everything is divided by 1048576 or 1000000 to go to megabits/mebibits
|
||||
if ((t + bonusT) / 1000.0 > settings.time_ul_max || failed) {
|
||||
// test is over, stop streams and timer
|
||||
if (failed || isNaN(ulStatus)) ulStatus = "Fail";
|
||||
clearRequests();
|
||||
clearInterval(interval);
|
||||
ulProgress = 1;
|
||||
tlog("ulTest: " + ulStatus + ", took " + (new Date().getTime() - startT) + "ms");
|
||||
done();
|
||||
}
|
||||
}
|
||||
}.bind(this),
|
||||
200
|
||||
);
|
||||
}.bind(this);
|
||||
if (settings.mpot) {
|
||||
tverb("Sending POST request before performing upload test");
|
||||
xhr = [];
|
||||
xhr[0] = new XMLHttpRequest();
|
||||
xhr[0].onload = xhr[0].onerror = function() {
|
||||
tverb("POST request sent, starting upload test");
|
||||
testFunction();
|
||||
}.bind(this);
|
||||
xhr[0].open("POST", settings.url_ul) + (settings.mpot ? "cors=true&" : "");
|
||||
xhr[0].send();
|
||||
} else testFunction();
|
||||
}
|
||||
// ping+jitter test, function done is called when it's over
|
||||
var ptCalled = false; // used to prevent multiple accidental calls to pingTest
|
||||
function pingTest(done) {
|
||||
tverb("pingTest");
|
||||
if (ptCalled) return;
|
||||
else ptCalled = true; // pingTest already called?
|
||||
var startT = new Date().getTime(); //when the test was started
|
||||
var prevT = null; // last time a pong was received
|
||||
var ping = 0.0; // current ping value
|
||||
var jitter = 0.0; // current jitter value
|
||||
var i = 0; // counter of pongs received
|
||||
var prevInstspd = 0; // last ping time, used for jitter calculation
|
||||
xhr = [];
|
||||
// ping function
|
||||
var doPing = function() {
|
||||
tverb("ping");
|
||||
pingProgress = i / settings.count_ping;
|
||||
prevT = new Date().getTime();
|
||||
xhr[0] = new XMLHttpRequest();
|
||||
xhr[0].onload = function() {
|
||||
// pong
|
||||
tverb("pong");
|
||||
if (i === 0) {
|
||||
prevT = new Date().getTime(); // first pong
|
||||
} else {
|
||||
var instspd = new Date().getTime() - prevT;
|
||||
if (settings.ping_allowPerformanceApi) {
|
||||
try {
|
||||
//try to get accurate performance timing using performance api
|
||||
var p = performance.getEntries();
|
||||
p = p[p.length - 1];
|
||||
var d = p.responseStart - p.requestStart;
|
||||
if (d <= 0) d = p.duration;
|
||||
if (d > 0 && d < instspd) instspd = d;
|
||||
} catch (e) {
|
||||
//if not possible, keep the estimate
|
||||
tverb("Performance API not supported, using estimate");
|
||||
}
|
||||
}
|
||||
//noticed that some browsers randomly have 0ms ping
|
||||
if (instspd < 1) instspd = prevInstspd;
|
||||
if (instspd < 1) instspd = 1;
|
||||
var instjitter = Math.abs(instspd - prevInstspd);
|
||||
if (i === 1) ping = instspd;
|
||||
/* first ping, can't tell jitter yet*/ else {
|
||||
if (instspd < ping) ping = instspd; // update ping, if the instant ping is lower
|
||||
if (i === 2) jitter = instjitter;
|
||||
//discard the first jitter measurement because it might be much higher than it should be
|
||||
else jitter = instjitter > jitter ? jitter * 0.3 + instjitter * 0.7 : jitter * 0.8 + instjitter * 0.2; // update jitter, weighted average. spikes in ping values are given more weight.
|
||||
}
|
||||
prevInstspd = instspd;
|
||||
}
|
||||
pingStatus = ping.toFixed(2);
|
||||
jitterStatus = jitter.toFixed(2);
|
||||
i++;
|
||||
tverb("ping: " + pingStatus + " jitter: " + jitterStatus);
|
||||
if (i < settings.count_ping) doPing();
|
||||
else {
|
||||
// more pings to do?
|
||||
pingProgress = 1;
|
||||
tlog("ping: " + pingStatus + " jitter: " + jitterStatus + ", took " + (new Date().getTime() - startT) + "ms");
|
||||
done();
|
||||
}
|
||||
}.bind(this);
|
||||
xhr[0].onerror = function() {
|
||||
// a ping failed, cancel test
|
||||
tverb("ping failed");
|
||||
if (settings.xhr_ignoreErrors === 0) {
|
||||
//abort
|
||||
pingStatus = "Fail";
|
||||
jitterStatus = "Fail";
|
||||
clearRequests();
|
||||
tlog("ping test failed, took " + (new Date().getTime() - startT) + "ms");
|
||||
pingProgress = 1;
|
||||
done();
|
||||
}
|
||||
if (settings.xhr_ignoreErrors === 1) doPing(); //retry ping
|
||||
if (settings.xhr_ignoreErrors === 2) {
|
||||
//ignore failed ping
|
||||
i++;
|
||||
if (i < settings.count_ping) doPing();
|
||||
else {
|
||||
// more pings to do?
|
||||
pingProgress = 1;
|
||||
tlog("ping: " + pingStatus + " jitter: " + jitterStatus + ", took " + (new Date().getTime() - startT) + "ms");
|
||||
done();
|
||||
}
|
||||
}
|
||||
}.bind(this);
|
||||
// send xhr
|
||||
xhr[0].open("GET", settings.url_ping + url_sep(settings.url_ping) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); // random string to prevent caching
|
||||
xhr[0].send();
|
||||
}.bind(this);
|
||||
doPing(); // start first ping
|
||||
}
|
||||
// telemetry
|
||||
function sendTelemetry(done) {
|
||||
if (settings.telemetry_level < 1) return;
|
||||
xhr = new XMLHttpRequest();
|
||||
xhr.onload = function() {
|
||||
try {
|
||||
var parts = xhr.responseText.split(" ");
|
||||
if (parts[0] == "id") {
|
||||
try {
|
||||
var id = parts[1];
|
||||
done(id);
|
||||
} catch (e) {
|
||||
done(null);
|
||||
}
|
||||
} else done(null);
|
||||
} catch (e) {
|
||||
done(null);
|
||||
}
|
||||
};
|
||||
xhr.onerror = function() {
|
||||
console.log("TELEMETRY ERROR " + xhr.status);
|
||||
done(null);
|
||||
};
|
||||
xhr.open("POST", settings.url_telemetry + url_sep(settings.url_telemetry) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true);
|
||||
var telemetryIspInfo = {
|
||||
processedString: clientIp,
|
||||
rawIspInfo: typeof ispInfo === "object" ? ispInfo : ""
|
||||
};
|
||||
try {
|
||||
var fd = new FormData();
|
||||
fd.append("ispinfo", JSON.stringify(telemetryIspInfo));
|
||||
fd.append("dl", dlStatus);
|
||||
fd.append("ul", ulStatus);
|
||||
fd.append("ping", pingStatus);
|
||||
fd.append("jitter", jitterStatus);
|
||||
fd.append("log", settings.telemetry_level > 1 ? log : "");
|
||||
fd.append("extra", settings.telemetry_extra);
|
||||
xhr.send(fd);
|
||||
} catch (ex) {
|
||||
var postData = "extra=" + encodeURIComponent(settings.telemetry_extra) + "&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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user