You've already forked speedtest-go
Sync PHP backend feature parity: IP detection, database backends, API endpoints, and frontend
- IP detection: Cloudflare IPv6, ULA IPv6, proxy header chain, offline GeoIP DB - Database: add SQLite (pure Go, no CGo) and MSSQL backends - API: add JSON result sharing endpoint and ID obfuscation - Frontend: add modern CSS design, design switcher, favicon - Compatibility: ?cors parameter support, human-friendly distance rounding - Update Go to 1.21, add modernc.org/sqlite and maxminddb deps
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Feature switch for enabling the new LibreSpeed design
|
||||
*
|
||||
* This script checks for:
|
||||
* 1. URL parameter: ?design=new or ?design=old
|
||||
* 2. Default behavior: Shows the classic design
|
||||
*
|
||||
* Note: This script is only loaded on the root index.html
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// Don't run this script if we're already on a specific design page
|
||||
const currentPath = window.location.pathname;
|
||||
if (currentPath.includes('index-classic.html') || currentPath.includes('index-modern.html')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check URL parameters first
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const designParam = urlParams.get('design');
|
||||
|
||||
if (designParam === 'new') {
|
||||
redirectToNewDesign();
|
||||
return;
|
||||
}
|
||||
|
||||
if (designParam === 'old' || designParam === 'classic') {
|
||||
redirectToOldDesign();
|
||||
return;
|
||||
}
|
||||
|
||||
// Default to classic design
|
||||
redirectToOldDesign();
|
||||
|
||||
function redirectToNewDesign() {
|
||||
const currentParams = window.location.search;
|
||||
window.location.href = 'index-modern.html' + currentParams;
|
||||
}
|
||||
|
||||
function redirectToOldDesign() {
|
||||
const currentParams = window.location.search;
|
||||
window.location.href = 'index-classic.html' + currentParams;
|
||||
}
|
||||
})();
|
||||
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
@@ -0,0 +1,22 @@
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_373_537)">
|
||||
<path
|
||||
d="M26 12L16 22L6 12"
|
||||
stroke="#625B6B"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_373_537">
|
||||
<rect width="32" height="32" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 500 B |
@@ -0,0 +1,14 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_373_731)">
|
||||
<path d="M25 7L7 25" stroke="#F5F5F5" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M25 25L7 7" stroke="#F5F5F5" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_373_731">
|
||||
<rect width="32" height="32" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 544 B |
@@ -0,0 +1,19 @@
|
||||
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M16.0345 0.123047H3.20289C1.88703 0.123047 0.820312 1.18976 0.820312 2.50562V15.3373C0.820312 16.6531 1.88703 17.7198 3.20289 17.7198H16.0345C17.3504 17.7198 18.4171 16.6531 18.4171 15.3373V2.50562C18.4171 1.18976 17.3504 0.123047 16.0345 0.123047Z"
|
||||
fill="#F5F5F5" />
|
||||
<path
|
||||
d="M17.1947 2.38135H3.55467L3.42638 9.32426L8.84467 9.49601L8.25888 9.99686L2.04297 15.4616L16.3914 15.3137V8.62233L10.1332 7.52898L17.1947 2.38135Z"
|
||||
fill="url(#paint0_linear_373_542)" />
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_373_542" x1="2.04297" y1="8.92149" x2="17.1947"
|
||||
y2="8.92149" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#D63BC6" />
|
||||
<stop offset="0.3478" stop-color="#7419B1" />
|
||||
<stop offset="0.5389" stop-color="#3D06A5" />
|
||||
<stop offset="0.7532" stop-color="#485DC4" />
|
||||
<stop offset="1" stop-color="#5CF9FD" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,66 @@
|
||||
<svg
|
||||
width="153"
|
||||
height="18"
|
||||
viewBox="0 0 153 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="logo"
|
||||
>
|
||||
<path
|
||||
d="M87.0345 0.123047H74.2029C72.887 0.123047 71.8203 1.18976 71.8203 2.50562V15.3373C71.8203 16.6531 72.887 17.7198 74.2029 17.7198H87.0345C88.3504 17.7198 89.4171 16.6531 89.4171 15.3373V2.50562C89.4171 1.18976 88.3504 0.123047 87.0345 0.123047Z"
|
||||
fill="#F5F5F5"
|
||||
/>
|
||||
<path
|
||||
d="M88.1947 2.38135H74.5547L74.4264 9.32426L79.8447 9.49601L79.2589 9.99686L73.043 15.4616L87.3914 15.3137V8.62233L81.1332 7.52898L88.1947 2.38135Z"
|
||||
fill="url(#paint0_linear_373_540)"
|
||||
/>
|
||||
<path
|
||||
d="M0 17.4134V0H3.68171V14.378H11.1473V17.4134H0Z"
|
||||
fill="#F5F5F5"
|
||||
/>
|
||||
<path d="M17.2686 0V17.4134H13.5869V0H17.2686Z" fill="#F5F5F5" />
|
||||
<path
|
||||
d="M20.3038 17.4134V0H27.2761C28.5572 0 29.6258 0.189989 30.4818 0.569601C31.3374 0.94958 31.9808 1.47506 32.4117 2.14682C32.8425 2.81857 33.058 3.59104 33.058 4.46387C33.058 5.14385 32.9218 5.74068 32.6497 6.25365C32.3776 6.76661 32.0048 7.18598 31.5317 7.51218C31.0582 7.83802 30.5183 8.06886 29.9118 8.20508V8.37499C30.5749 8.40366 31.1972 8.59042 31.7783 8.93633C32.3593 9.28225 32.8311 9.76544 33.1938 10.3859C33.5566 11.0068 33.7379 11.7452 33.7379 12.6008C33.7379 13.5249 33.51 14.3483 33.0536 15.0709C32.5973 15.7936 31.9242 16.365 31.0341 16.7844C30.144 17.2038 29.0475 17.4135 27.7438 17.4135L20.3038 17.4134ZM23.9855 7.28666H26.7148C27.2195 7.28666 27.6686 7.19739 28.0626 7.01889C28.4565 6.84039 28.7684 6.58661 28.9978 6.25791C29.2276 5.92921 29.3423 5.53524 29.3423 5.07609C29.3423 4.44666 29.1197 3.93942 28.6748 3.55408C28.2296 3.16874 27.5991 2.97589 26.7829 2.97589H23.9855V7.28666ZM23.9855 14.4034H26.9869C28.0127 14.4034 28.7612 14.2067 29.2319 13.8123C29.7021 13.4184 29.9373 12.8929 29.9373 12.2351C29.9373 11.7534 29.8212 11.3282 29.5889 10.9597C29.3563 10.5913 29.0261 10.3023 28.5981 10.0926C28.1701 9.88294 27.6615 9.77792 27.0722 9.77792H23.9855L23.9855 14.4034Z"
|
||||
fill="#F5F5F5"
|
||||
/>
|
||||
<path
|
||||
d="M36.127 17.4134V0H42.9971C44.3123 0 45.436 0.233706 46.3684 0.701485C47.3011 1.1689 48.0123 1.82953 48.5027 2.68232C48.993 3.5358 49.2382 4.53769 49.2382 5.68834C49.2382 6.84472 48.9887 7.83798 48.4901 8.66853C47.9911 9.4987 47.2699 10.135 46.3261 10.5773C45.3823 11.0193 44.2413 11.2404 42.9036 11.2404H38.3039V8.28139H42.3086C43.0115 8.28139 43.5955 8.18533 44.06 7.99248C44.5249 7.79963 44.8723 7.51071 45.1017 7.12501C45.3315 6.73967 45.4462 6.26077 45.4462 5.68831C45.4462 5.11011 45.3315 4.62262 45.1017 4.2258C44.8723 3.82897 44.5235 3.52717 44.0557 3.31997C43.5883 3.11313 43.0001 3.00992 42.2917 3.00992H39.8087V17.4134L36.127 17.4134ZM45.531 9.48901L49.859 17.4134H45.7945L41.5604 9.48901H45.531Z"
|
||||
fill="#F5F5F5"
|
||||
/>
|
||||
<path
|
||||
d="M51.8398 17.4134V0H63.5735V3.03539H55.5216V7.18451H62.9699V10.2203H55.5216V14.378H63.6076V17.4134L51.8398 17.4134Z"
|
||||
fill="#F5F5F5"
|
||||
/>
|
||||
<path
|
||||
d="M92.8662 17.4134V0H99.7364C101.058 0 102.182 0.250554 103.112 0.752396C104.042 1.25387 104.752 1.94857 105.242 2.83538C105.733 3.72256 105.978 4.74452 105.978 5.90091C105.978 7.05729 105.728 8.07745 105.229 8.96177C104.73 9.84609 104.009 10.5347 103.066 11.0279C102.122 11.5212 100.981 11.7677 99.6432 11.7677H95.2643V8.8173H99.0481C99.7564 8.8173 100.341 8.694 100.804 8.44737C101.266 8.20075 101.612 7.85773 101.841 7.41862C102.071 6.97914 102.185 6.47337 102.185 5.90091C102.185 5.32272 102.071 4.81658 101.841 4.38319C101.612 3.94948 101.264 3.61215 100.8 3.37129C100.335 3.13042 99.745 3.00995 99.0309 3.00995H96.5482V17.4134H92.8662Z"
|
||||
fill="#F5F5F5"
|
||||
/>
|
||||
<path
|
||||
d="M108.375 17.4134V0H120.109V3.03539H112.057V7.18451H119.505V10.2203H112.057V14.378H120.143V17.4134L108.375 17.4134Z"
|
||||
fill="#F5F5F5"
|
||||
/>
|
||||
<path
|
||||
d="M123.042 17.4134V0H134.776V3.03539H126.723V7.18451H134.172V10.2203H126.723V14.378H134.809V17.4134L123.042 17.4134Z"
|
||||
fill="#F5F5F5"
|
||||
/>
|
||||
<path
|
||||
d="M143.882 17.4134H137.709V0H143.933C145.685 0 147.192 0.34698 148.457 1.04131C149.721 1.736 150.694 2.73213 151.378 4.03012C152.06 5.32844 152.402 6.88165 152.402 8.68967C152.402 10.5035 152.06 12.0624 151.378 13.3661C150.694 14.6698 149.716 15.6702 148.444 16.3674C147.171 17.0647 145.651 17.4134 143.882 17.4134ZM141.391 14.259H143.729C144.817 14.259 145.734 14.0647 146.48 13.6765C147.225 13.2883 147.787 12.6847 148.163 11.8652C148.54 11.0465 148.729 9.98763 148.729 8.68963C148.729 7.40276 148.54 6.35143 148.163 5.53521C147.787 4.71865 147.227 4.11788 146.484 3.73254C145.741 3.34721 144.826 3.15435 143.737 3.15435H141.391L141.391 14.259Z"
|
||||
fill="#F5F5F5"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_373_540"
|
||||
x1="73.043"
|
||||
y1="8.92149"
|
||||
x2="88.1947"
|
||||
y2="8.92149"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#D63BC6" />
|
||||
<stop offset="0.3478" stop-color="#7419B1" />
|
||||
<stop offset="0.5389" stop-color="#3D06A5" />
|
||||
<stop offset="0.7532" stop-color="#485DC4" />
|
||||
<stop offset="1" stop-color="#5CF9FD" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
Executable
+365
@@ -0,0 +1,365 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no" />
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<script type="text/javascript" src="speedtest.js"></script>
|
||||
<script type="text/javascript">
|
||||
function I(i){return document.getElementById(i);}
|
||||
//INITIALIZE SPEEDTEST
|
||||
var s=new Speedtest(); //create speedtest object
|
||||
s.setParameter("telemetry_level","basic"); //enable telemetry
|
||||
|
||||
var meterBk=/Trident.*rv:(\d+\.\d+)/i.test(navigator.userAgent)?"#EAEAEA":"#80808040";
|
||||
var dlColor="#6060AA",
|
||||
ulColor="#616161";
|
||||
var progColor=meterBk;
|
||||
|
||||
//CODE FOR GAUGES
|
||||
function drawMeter(c,amount,bk,fg,progress,prog){
|
||||
var ctx=c.getContext("2d");
|
||||
var dp=window.devicePixelRatio||1;
|
||||
var cw=c.clientWidth*dp, ch=c.clientHeight*dp;
|
||||
var sizScale=ch*0.0055;
|
||||
if(c.width==cw&&c.height==ch){
|
||||
ctx.clearRect(0,0,cw,ch);
|
||||
}else{
|
||||
c.width=cw;
|
||||
c.height=ch;
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle=bk;
|
||||
ctx.lineWidth=12*sizScale;
|
||||
ctx.arc(c.width/2,c.height-58*sizScale,c.height/1.8-ctx.lineWidth,-Math.PI*1.1,Math.PI*0.1);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle=fg;
|
||||
ctx.lineWidth=12*sizScale;
|
||||
ctx.arc(c.width/2,c.height-58*sizScale,c.height/1.8-ctx.lineWidth,-Math.PI*1.1,amount*Math.PI*1.2-Math.PI*1.1);
|
||||
ctx.stroke();
|
||||
if(typeof progress !== "undefined"){
|
||||
ctx.fillStyle=prog;
|
||||
ctx.fillRect(c.width*0.3,c.height-16*sizScale,c.width*0.4*progress,4*sizScale);
|
||||
}
|
||||
}
|
||||
function mbpsToAmount(s){
|
||||
return 1-(1/(Math.pow(1.3,Math.sqrt(s))));
|
||||
}
|
||||
function format(d){
|
||||
d=Number(d);
|
||||
if(d<10) return d.toFixed(2);
|
||||
if(d<100) return d.toFixed(1);
|
||||
return d.toFixed(0);
|
||||
}
|
||||
|
||||
//UI CODE
|
||||
var uiData=null;
|
||||
function startStop(){
|
||||
if(s.getState()==3){
|
||||
//speedtest is running, abort
|
||||
s.abort();
|
||||
data=null;
|
||||
I("startStopBtn").className="";
|
||||
initUI();
|
||||
}else{
|
||||
//test is not running, begin
|
||||
I("startStopBtn").className="running";
|
||||
I("shareArea").style.display="none";
|
||||
s.onupdate=function(data){
|
||||
uiData=data;
|
||||
};
|
||||
s.onend=function(aborted){
|
||||
I("startStopBtn").className="";
|
||||
updateUI(true);
|
||||
if(!aborted){
|
||||
//if testId is present, show sharing panel, otherwise do nothing
|
||||
try{
|
||||
var testId=uiData.testId;
|
||||
if(testId!=null){
|
||||
var shareURL=window.location.href.substring(0,window.location.href.lastIndexOf("/"))+"/results/?id="+testId;
|
||||
I("resultsImg").src=shareURL;
|
||||
I("resultsURL").value=shareURL;
|
||||
I("testId").innerHTML=testId;
|
||||
I("shareArea").style.display="";
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
};
|
||||
s.start();
|
||||
}
|
||||
}
|
||||
//this function reads the data sent back by the test and updates the UI
|
||||
function updateUI(forced){
|
||||
if(!forced&&s.getState()!=3) return;
|
||||
if(uiData==null) return;
|
||||
var status=uiData.testState;
|
||||
I("ip").textContent=uiData.clientIp;
|
||||
I("dlText").textContent=(status==1&&uiData.dlStatus==0)?"...":format(uiData.dlStatus);
|
||||
drawMeter(I("dlMeter"),mbpsToAmount(Number(uiData.dlStatus*(status==1?oscillate():1))),meterBk,dlColor,Number(uiData.dlProgress),progColor);
|
||||
I("ulText").textContent=(status==3&&uiData.ulStatus==0)?"...":format(uiData.ulStatus);
|
||||
drawMeter(I("ulMeter"),mbpsToAmount(Number(uiData.ulStatus*(status==3?oscillate():1))),meterBk,ulColor,Number(uiData.ulProgress),progColor);
|
||||
I("pingText").textContent=format(uiData.pingStatus);
|
||||
I("jitText").textContent=format(uiData.jitterStatus);
|
||||
}
|
||||
function oscillate(){
|
||||
return 1+0.02*Math.sin(Date.now()/100);
|
||||
}
|
||||
//update the UI every frame
|
||||
window.requestAnimationFrame=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.msRequestAnimationFrame||(function(callback,element){setTimeout(callback,1000/60);});
|
||||
function frame(){
|
||||
requestAnimationFrame(frame);
|
||||
updateUI();
|
||||
}
|
||||
frame(); //start frame loop
|
||||
//function to (re)initialize UI
|
||||
function initUI(){
|
||||
drawMeter(I("dlMeter"),0,meterBk,dlColor,0);
|
||||
drawMeter(I("ulMeter"),0,meterBk,ulColor,0);
|
||||
I("dlText").textContent="";
|
||||
I("ulText").textContent="";
|
||||
I("pingText").textContent="";
|
||||
I("jitText").textContent="";
|
||||
I("ip").textContent="";
|
||||
}
|
||||
</script>
|
||||
<style type="text/css">
|
||||
html,body{
|
||||
border:none; padding:0; margin:0;
|
||||
background:#FFFFFF;
|
||||
color:#202020;
|
||||
}
|
||||
body{
|
||||
text-align:center;
|
||||
font-family:"Roboto",sans-serif;
|
||||
}
|
||||
h1{
|
||||
color:#404040;
|
||||
}
|
||||
#startStopBtn{
|
||||
display:inline-block;
|
||||
margin:0 auto;
|
||||
color:#6060AA;
|
||||
background-color:rgba(0,0,0,0);
|
||||
border:0.15em solid #6060FF;
|
||||
border-radius:0.3em;
|
||||
transition:all 0.3s;
|
||||
box-sizing:border-box;
|
||||
width:8em; height:3em;
|
||||
line-height:2.7em;
|
||||
cursor:pointer;
|
||||
box-shadow: 0 0 0 rgba(0,0,0,0.1), inset 0 0 0 rgba(0,0,0,0.1);
|
||||
}
|
||||
#startStopBtn:hover{
|
||||
box-shadow: 0 0 2em rgba(0,0,0,0.1), inset 0 0 1em rgba(0,0,0,0.1);
|
||||
}
|
||||
#startStopBtn.running{
|
||||
background-color:#FF3030;
|
||||
border-color:#FF6060;
|
||||
color:#FFFFFF;
|
||||
}
|
||||
#startStopBtn:before{
|
||||
content:"Start";
|
||||
}
|
||||
#startStopBtn.running:before{
|
||||
content:"Abort";
|
||||
}
|
||||
#test{
|
||||
margin-top:2em;
|
||||
margin-bottom:12em;
|
||||
}
|
||||
div.testArea{
|
||||
display:inline-block;
|
||||
width:16em;
|
||||
height:12.5em;
|
||||
position:relative;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
div.testArea2{
|
||||
display:inline-block;
|
||||
width:14em;
|
||||
height:7em;
|
||||
position:relative;
|
||||
box-sizing:border-box;
|
||||
text-align:center;
|
||||
}
|
||||
div.testArea div.testName{
|
||||
position:absolute;
|
||||
top:0.1em; left:0;
|
||||
width:100%;
|
||||
font-size:1.4em;
|
||||
z-index:9;
|
||||
}
|
||||
div.testArea2 div.testName{
|
||||
display:block;
|
||||
text-align:center;
|
||||
font-size:1.4em;
|
||||
}
|
||||
div.testArea div.meterText{
|
||||
position:absolute;
|
||||
bottom:1.55em; left:0;
|
||||
width:100%;
|
||||
font-size:2.5em;
|
||||
z-index:9;
|
||||
}
|
||||
div.testArea2 div.meterText{
|
||||
display:inline-block;
|
||||
font-size:2.5em;
|
||||
}
|
||||
div.meterText:empty:before{
|
||||
content:"0.00";
|
||||
}
|
||||
div.testArea div.unit{
|
||||
position:absolute;
|
||||
bottom:2em; left:0;
|
||||
width:100%;
|
||||
z-index:9;
|
||||
}
|
||||
div.testArea2 div.unit{
|
||||
display:inline-block;
|
||||
}
|
||||
div.testArea canvas{
|
||||
position:absolute;
|
||||
top:0; left:0; width:100%; height:100%;
|
||||
z-index:1;
|
||||
}
|
||||
div.testGroup{
|
||||
display:block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
#shareArea{
|
||||
width:95%;
|
||||
max-width:40em;
|
||||
margin:0 auto;
|
||||
margin-top:2em;
|
||||
}
|
||||
#shareArea > *{
|
||||
display:block;
|
||||
width:100%;
|
||||
height:auto;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
#privacyPolicy{
|
||||
position:fixed;
|
||||
top:2em;
|
||||
bottom:2em;
|
||||
left:2em;
|
||||
right:2em;
|
||||
overflow-y:auto;
|
||||
width:auto;
|
||||
height:auto;
|
||||
box-shadow:0 0 3em 1em #000000;
|
||||
z-index:999999;
|
||||
text-align:left;
|
||||
background-color:#FFFFFF;
|
||||
padding:1em;
|
||||
}
|
||||
a.privacy{
|
||||
text-align:center;
|
||||
font-size:0.8em;
|
||||
color:#808080;
|
||||
padding: 0 3em;
|
||||
}
|
||||
div.closePrivacyPolicy {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
div.closePrivacyPolicy a.privacy {
|
||||
padding: 1em 3em;
|
||||
}
|
||||
@media all and (max-width:40em){
|
||||
body{
|
||||
font-size:0.8em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<title>LibreSpeed Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>LibreSpeed Example</h1>
|
||||
<div id="testWrapper">
|
||||
<div id="startStopBtn" onclick="startStop()"></div><br/>
|
||||
<a class="privacy" href="#" onclick="I('privacyPolicy').style.display=''">Privacy</a>
|
||||
<div id="test">
|
||||
<div class="testGroup">
|
||||
<div class="testArea2">
|
||||
<div class="testName">Ping</div>
|
||||
<div id="pingText" class="meterText" style="color:#AA6060"></div>
|
||||
<div class="unit">ms</div>
|
||||
</div>
|
||||
<div class="testArea2">
|
||||
<div class="testName">Jitter</div>
|
||||
<div id="jitText" class="meterText" style="color:#AA6060"></div>
|
||||
<div class="unit">ms</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="testGroup">
|
||||
<div class="testArea">
|
||||
<div class="testName">Download</div>
|
||||
<canvas id="dlMeter" class="meter"></canvas>
|
||||
<div id="dlText" class="meterText"></div>
|
||||
<div class="unit">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 id="ipArea">
|
||||
<span id="ip"></span>
|
||||
</div>
|
||||
<div id="shareArea" style="display:none">
|
||||
<h3>Share results</h3>
|
||||
<p>Test ID: <span id="testId"></span></p>
|
||||
<input type="text" value="" id="resultsURL" readonly="readonly" onclick="this.select();this.focus();this.select();document.execCommand('copy');alert('Link copied')"/>
|
||||
<img src="" id="resultsImg" />
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://github.com/librespeed/speedtest">Source code</a>
|
||||
</div>
|
||||
<div id="privacyPolicy" style="display:none">
|
||||
<h2>Privacy Policy</h2>
|
||||
<p>This HTML5 Speedtest server is configured with telemetry enabled.</p>
|
||||
<h4>What data we collect</h4>
|
||||
<p>
|
||||
At the end of the test, the following data is collected and stored:
|
||||
<ul>
|
||||
<li>Test ID</li>
|
||||
<li>Time of testing</li>
|
||||
<li>Test results (download and upload speed, ping and jitter)</li>
|
||||
<li>IP address</li>
|
||||
<li>ISP information</li>
|
||||
<li>Approximate location (inferred from IP address, not GPS)</li>
|
||||
<li>User agent and browser locale</li>
|
||||
<li>Test log (contains no personal information)</li>
|
||||
</ul>
|
||||
</p>
|
||||
<h4>How we use the data</h4>
|
||||
<p>
|
||||
Data collected through this service is used to:
|
||||
<ul>
|
||||
<li>Allow sharing of test results (sharable image for forums, etc.)</li>
|
||||
<li>To improve the service offered to you (for instance, to detect problems on our side)</li>
|
||||
</ul>
|
||||
No personal information is disclosed to third parties.
|
||||
</p>
|
||||
<h4>Your consent</h4>
|
||||
<p>
|
||||
By starting the test, you consent to the terms of this privacy policy.
|
||||
</p>
|
||||
<h4>Data removal</h4>
|
||||
<p>
|
||||
If you want to have your information deleted, you need to provide either the ID of the test or your IP address. This is the only way to identify your data, without this information we won't be able to comply with your request.<br/><br/>
|
||||
Contact this email address for all deletion requests: <a href="mailto:PUT@YOUR_EMAIL.HERE">TO BE FILLED BY DEVELOPER</a>.
|
||||
</p>
|
||||
<br/><br/>
|
||||
<div class="closePrivacyPolicy">
|
||||
<a class="privacy" href="#" onclick="I('privacyPolicy').style.display='none'">Close</a>
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
<script type="text/javascript">setTimeout(function(){initUI()},100);</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<meta name="description"
|
||||
content="Free and Open Source Speedtest. Run it right now in your browser, or self-host on a PHP, Golang, Rust or Node server. License: LGPL." />
|
||||
<link rel="shortcut icon" href="images/favicon.svg" />
|
||||
<script type="text/javascript" src="speedtest.js"></script>
|
||||
<script type="text/javascript">
|
||||
// Set this to a different URL to load the server list from another location.
|
||||
var SPEEDTEST_SERVERS = "server-list.json";
|
||||
</script>
|
||||
<script type="text/javascript" src="javascript/index.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="styling/index.css" />
|
||||
<title>LibreSpeed - Free and Open Source Speedtest</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<img src="images/logo.svg" alt="LibreSpeed" />
|
||||
</header>
|
||||
<main>
|
||||
<h1>Free and Open Source Speedtest.</h1>
|
||||
<p class="tagline">No Flash, No Java, No Websockets, No Bullsh*t</p>
|
||||
|
||||
<div class="server-selector">
|
||||
<div class="chosen">
|
||||
<div class="chevron">
|
||||
<img src="images/chevron.svg" alt="select..." />
|
||||
</div>
|
||||
<p>current server</p>
|
||||
<h2 id="selected-server">searching nearest server...</h2>
|
||||
</div>
|
||||
<ul class="servers"></ul>
|
||||
<p class="sponsor" id="sponsor"> </p>
|
||||
</div>
|
||||
|
||||
<p id="privacy-warning" class="hidden">
|
||||
by clicking the start button you agree to our privacy policy<br />
|
||||
<a href="#" id="choose-privacy">or choose your privacy options</a>
|
||||
</p>
|
||||
<button class="disabled" id="start-button"></button>
|
||||
|
||||
<div class="gauge-layout">
|
||||
<div class="ping hidden">
|
||||
<span class="label">Ping</span>:
|
||||
<span class="value" id="ping">00</span>ms
|
||||
</div>
|
||||
|
||||
<div class="gauge download" id="download-gauge">
|
||||
<div class="progress"></div>
|
||||
<div class="speed"></div>
|
||||
<h1><span id="download-speed">00</span> Mbps</h1>
|
||||
<h2>Download</h2>
|
||||
</div>
|
||||
|
||||
<div class="gauge upload" id="upload-gauge">
|
||||
<div class="progress"></div>
|
||||
<div class="speed"></div>
|
||||
<h1><span id="upload-speed">00</span> Mbps</h1>
|
||||
<h2>Upload</h2>
|
||||
</div>
|
||||
|
||||
<div class="jitter hidden">
|
||||
<span class="label">Jitter</span>:
|
||||
<span class="value" id="jitter">00</span>ms
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="small inverted hidden" id="share-results">
|
||||
Share results
|
||||
</button>
|
||||
</main>
|
||||
<footer>
|
||||
<p class="source">
|
||||
<a href="https://github.com/librespeed/speedtest">source code</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<dialog id="share">
|
||||
<div class="close-dialog">
|
||||
<img src="images/close-button.svg" alt="Close" />
|
||||
</div>
|
||||
<img id="results" src="" alt="Test results in graphical form" />
|
||||
<button id="copy-link">Copy link</button>
|
||||
</dialog>
|
||||
|
||||
<dialog id="privacy">
|
||||
<div class="close-dialog">
|
||||
<img src="images/close-button.svg" alt="Close" />
|
||||
</div>
|
||||
<section>
|
||||
<h1>Privacy Policy</h1>
|
||||
<p>
|
||||
This HTML5 speed test server is configured with telemetry enabled.
|
||||
</p>
|
||||
|
||||
<h2>What data we collect</h2>
|
||||
<p>
|
||||
At the end of the test, the following data is collected and stored:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>Test ID</li>
|
||||
<li>Time of testing</li>
|
||||
<li>Test results (download and upload speed, ping and jitter)</li>
|
||||
<li>IP address</li>
|
||||
<li>ISP information</li>
|
||||
<li>Approximate location (inferred from IP address, not GPS)</li>
|
||||
<li>User agent and browser locale</li>
|
||||
<li>Test log (contains no personal information)</li>
|
||||
</ul>
|
||||
|
||||
<h2>How we use the data</h2>
|
||||
<p>Data collected through this service is used to:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
Allow sharing of test results (sharable image for forums, etc.)
|
||||
</li>
|
||||
<li>
|
||||
To improve the service offered to you (for instance, to detect
|
||||
problems on our side)
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>No personal information is disclosed to third parties.</p>
|
||||
|
||||
<h2>Your consent</h2>
|
||||
<p>
|
||||
By starting the test, you consent to the terms of this privacy policy.
|
||||
</p>
|
||||
|
||||
<h2>Data removal</h2>
|
||||
<p>
|
||||
If you want to have your information deleted, you need to provide
|
||||
either the ID of the test or your IP address. This is the only way to
|
||||
identify your data, without this information we won't be able to
|
||||
comply with your request.
|
||||
</p>
|
||||
<p>
|
||||
Contact this email address for all deletion requests:
|
||||
<a href="mailto:PUT@YOUR_EMAIL.HERE">TO BE FILLED BY DEVELOPER</a>.
|
||||
</p>
|
||||
</section>
|
||||
<button id="close-privacy">Close</button>
|
||||
</dialog>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Executable → Regular
+9
-358
@@ -1,365 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no" />
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<script type="text/javascript" src="speedtest.js"></script>
|
||||
<script type="text/javascript">
|
||||
function I(i){return document.getElementById(i);}
|
||||
//INITIALIZE SPEEDTEST
|
||||
var s=new Speedtest(); //create speedtest object
|
||||
s.setParameter("telemetry_level","basic"); //enable telemetry
|
||||
|
||||
var meterBk=/Trident.*rv:(\d+\.\d+)/i.test(navigator.userAgent)?"#EAEAEA":"#80808040";
|
||||
var dlColor="#6060AA",
|
||||
ulColor="#616161";
|
||||
var progColor=meterBk;
|
||||
|
||||
//CODE FOR GAUGES
|
||||
function drawMeter(c,amount,bk,fg,progress,prog){
|
||||
var ctx=c.getContext("2d");
|
||||
var dp=window.devicePixelRatio||1;
|
||||
var cw=c.clientWidth*dp, ch=c.clientHeight*dp;
|
||||
var sizScale=ch*0.0055;
|
||||
if(c.width==cw&&c.height==ch){
|
||||
ctx.clearRect(0,0,cw,ch);
|
||||
}else{
|
||||
c.width=cw;
|
||||
c.height=ch;
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle=bk;
|
||||
ctx.lineWidth=12*sizScale;
|
||||
ctx.arc(c.width/2,c.height-58*sizScale,c.height/1.8-ctx.lineWidth,-Math.PI*1.1,Math.PI*0.1);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle=fg;
|
||||
ctx.lineWidth=12*sizScale;
|
||||
ctx.arc(c.width/2,c.height-58*sizScale,c.height/1.8-ctx.lineWidth,-Math.PI*1.1,amount*Math.PI*1.2-Math.PI*1.1);
|
||||
ctx.stroke();
|
||||
if(typeof progress !== "undefined"){
|
||||
ctx.fillStyle=prog;
|
||||
ctx.fillRect(c.width*0.3,c.height-16*sizScale,c.width*0.4*progress,4*sizScale);
|
||||
}
|
||||
}
|
||||
function mbpsToAmount(s){
|
||||
return 1-(1/(Math.pow(1.3,Math.sqrt(s))));
|
||||
}
|
||||
function format(d){
|
||||
d=Number(d);
|
||||
if(d<10) return d.toFixed(2);
|
||||
if(d<100) return d.toFixed(1);
|
||||
return d.toFixed(0);
|
||||
}
|
||||
|
||||
//UI CODE
|
||||
var uiData=null;
|
||||
function startStop(){
|
||||
if(s.getState()==3){
|
||||
//speedtest is running, abort
|
||||
s.abort();
|
||||
data=null;
|
||||
I("startStopBtn").className="";
|
||||
initUI();
|
||||
}else{
|
||||
//test is not running, begin
|
||||
I("startStopBtn").className="running";
|
||||
I("shareArea").style.display="none";
|
||||
s.onupdate=function(data){
|
||||
uiData=data;
|
||||
};
|
||||
s.onend=function(aborted){
|
||||
I("startStopBtn").className="";
|
||||
updateUI(true);
|
||||
if(!aborted){
|
||||
//if testId is present, show sharing panel, otherwise do nothing
|
||||
try{
|
||||
var testId=uiData.testId;
|
||||
if(testId!=null){
|
||||
var shareURL=window.location.href.substring(0,window.location.href.lastIndexOf("/"))+"/results/?id="+testId;
|
||||
I("resultsImg").src=shareURL;
|
||||
I("resultsURL").value=shareURL;
|
||||
I("testId").innerHTML=testId;
|
||||
I("shareArea").style.display="";
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
};
|
||||
s.start();
|
||||
}
|
||||
}
|
||||
//this function reads the data sent back by the test and updates the UI
|
||||
function updateUI(forced){
|
||||
if(!forced&&s.getState()!=3) return;
|
||||
if(uiData==null) return;
|
||||
var status=uiData.testState;
|
||||
I("ip").textContent=uiData.clientIp;
|
||||
I("dlText").textContent=(status==1&&uiData.dlStatus==0)?"...":format(uiData.dlStatus);
|
||||
drawMeter(I("dlMeter"),mbpsToAmount(Number(uiData.dlStatus*(status==1?oscillate():1))),meterBk,dlColor,Number(uiData.dlProgress),progColor);
|
||||
I("ulText").textContent=(status==3&&uiData.ulStatus==0)?"...":format(uiData.ulStatus);
|
||||
drawMeter(I("ulMeter"),mbpsToAmount(Number(uiData.ulStatus*(status==3?oscillate():1))),meterBk,ulColor,Number(uiData.ulProgress),progColor);
|
||||
I("pingText").textContent=format(uiData.pingStatus);
|
||||
I("jitText").textContent=format(uiData.jitterStatus);
|
||||
}
|
||||
function oscillate(){
|
||||
return 1+0.02*Math.sin(Date.now()/100);
|
||||
}
|
||||
//update the UI every frame
|
||||
window.requestAnimationFrame=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.msRequestAnimationFrame||(function(callback,element){setTimeout(callback,1000/60);});
|
||||
function frame(){
|
||||
requestAnimationFrame(frame);
|
||||
updateUI();
|
||||
}
|
||||
frame(); //start frame loop
|
||||
//function to (re)initialize UI
|
||||
function initUI(){
|
||||
drawMeter(I("dlMeter"),0,meterBk,dlColor,0);
|
||||
drawMeter(I("ulMeter"),0,meterBk,ulColor,0);
|
||||
I("dlText").textContent="";
|
||||
I("ulText").textContent="";
|
||||
I("pingText").textContent="";
|
||||
I("jitText").textContent="";
|
||||
I("ip").textContent="";
|
||||
}
|
||||
</script>
|
||||
<style type="text/css">
|
||||
html,body{
|
||||
border:none; padding:0; margin:0;
|
||||
background:#FFFFFF;
|
||||
color:#202020;
|
||||
}
|
||||
body{
|
||||
text-align:center;
|
||||
font-family:"Roboto",sans-serif;
|
||||
}
|
||||
h1{
|
||||
color:#404040;
|
||||
}
|
||||
#startStopBtn{
|
||||
display:inline-block;
|
||||
margin:0 auto;
|
||||
color:#6060AA;
|
||||
background-color:rgba(0,0,0,0);
|
||||
border:0.15em solid #6060FF;
|
||||
border-radius:0.3em;
|
||||
transition:all 0.3s;
|
||||
box-sizing:border-box;
|
||||
width:8em; height:3em;
|
||||
line-height:2.7em;
|
||||
cursor:pointer;
|
||||
box-shadow: 0 0 0 rgba(0,0,0,0.1), inset 0 0 0 rgba(0,0,0,0.1);
|
||||
}
|
||||
#startStopBtn:hover{
|
||||
box-shadow: 0 0 2em rgba(0,0,0,0.1), inset 0 0 1em rgba(0,0,0,0.1);
|
||||
}
|
||||
#startStopBtn.running{
|
||||
background-color:#FF3030;
|
||||
border-color:#FF6060;
|
||||
color:#FFFFFF;
|
||||
}
|
||||
#startStopBtn:before{
|
||||
content:"Start";
|
||||
}
|
||||
#startStopBtn.running:before{
|
||||
content:"Abort";
|
||||
}
|
||||
#test{
|
||||
margin-top:2em;
|
||||
margin-bottom:12em;
|
||||
}
|
||||
div.testArea{
|
||||
display:inline-block;
|
||||
width:16em;
|
||||
height:12.5em;
|
||||
position:relative;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
div.testArea2{
|
||||
display:inline-block;
|
||||
width:14em;
|
||||
height:7em;
|
||||
position:relative;
|
||||
box-sizing:border-box;
|
||||
text-align:center;
|
||||
}
|
||||
div.testArea div.testName{
|
||||
position:absolute;
|
||||
top:0.1em; left:0;
|
||||
width:100%;
|
||||
font-size:1.4em;
|
||||
z-index:9;
|
||||
}
|
||||
div.testArea2 div.testName{
|
||||
display:block;
|
||||
text-align:center;
|
||||
font-size:1.4em;
|
||||
}
|
||||
div.testArea div.meterText{
|
||||
position:absolute;
|
||||
bottom:1.55em; left:0;
|
||||
width:100%;
|
||||
font-size:2.5em;
|
||||
z-index:9;
|
||||
}
|
||||
div.testArea2 div.meterText{
|
||||
display:inline-block;
|
||||
font-size:2.5em;
|
||||
}
|
||||
div.meterText:empty:before{
|
||||
content:"0.00";
|
||||
}
|
||||
div.testArea div.unit{
|
||||
position:absolute;
|
||||
bottom:2em; left:0;
|
||||
width:100%;
|
||||
z-index:9;
|
||||
}
|
||||
div.testArea2 div.unit{
|
||||
display:inline-block;
|
||||
}
|
||||
div.testArea canvas{
|
||||
position:absolute;
|
||||
top:0; left:0; width:100%; height:100%;
|
||||
z-index:1;
|
||||
}
|
||||
div.testGroup{
|
||||
display:block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
#shareArea{
|
||||
width:95%;
|
||||
max-width:40em;
|
||||
margin:0 auto;
|
||||
margin-top:2em;
|
||||
}
|
||||
#shareArea > *{
|
||||
display:block;
|
||||
width:100%;
|
||||
height:auto;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
#privacyPolicy{
|
||||
position:fixed;
|
||||
top:2em;
|
||||
bottom:2em;
|
||||
left:2em;
|
||||
right:2em;
|
||||
overflow-y:auto;
|
||||
width:auto;
|
||||
height:auto;
|
||||
box-shadow:0 0 3em 1em #000000;
|
||||
z-index:999999;
|
||||
text-align:left;
|
||||
background-color:#FFFFFF;
|
||||
padding:1em;
|
||||
}
|
||||
a.privacy{
|
||||
text-align:center;
|
||||
font-size:0.8em;
|
||||
color:#808080;
|
||||
padding: 0 3em;
|
||||
}
|
||||
div.closePrivacyPolicy {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
div.closePrivacyPolicy a.privacy {
|
||||
padding: 1em 3em;
|
||||
}
|
||||
@media all and (max-width:40em){
|
||||
body{
|
||||
font-size:0.8em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<title>LibreSpeed Example</title>
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no" />
|
||||
<meta charset="UTF-8" />
|
||||
<script type="text/javascript" src="design-switch.js"></script>
|
||||
<title>LibreSpeed</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>LibreSpeed Example</h1>
|
||||
<div id="testWrapper">
|
||||
<div id="startStopBtn" onclick="startStop()"></div><br/>
|
||||
<a class="privacy" href="#" onclick="I('privacyPolicy').style.display=''">Privacy</a>
|
||||
<div id="test">
|
||||
<div class="testGroup">
|
||||
<div class="testArea2">
|
||||
<div class="testName">Ping</div>
|
||||
<div id="pingText" class="meterText" style="color:#AA6060"></div>
|
||||
<div class="unit">ms</div>
|
||||
</div>
|
||||
<div class="testArea2">
|
||||
<div class="testName">Jitter</div>
|
||||
<div id="jitText" class="meterText" style="color:#AA6060"></div>
|
||||
<div class="unit">ms</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="testGroup">
|
||||
<div class="testArea">
|
||||
<div class="testName">Download</div>
|
||||
<canvas id="dlMeter" class="meter"></canvas>
|
||||
<div id="dlText" class="meterText"></div>
|
||||
<div class="unit">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 id="ipArea">
|
||||
<span id="ip"></span>
|
||||
</div>
|
||||
<div id="shareArea" style="display:none">
|
||||
<h3>Share results</h3>
|
||||
<p>Test ID: <span id="testId"></span></p>
|
||||
<input type="text" value="" id="resultsURL" readonly="readonly" onclick="this.select();this.focus();this.select();document.execCommand('copy');alert('Link copied')"/>
|
||||
<img src="" id="resultsImg" />
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://github.com/librespeed/speedtest">Source code</a>
|
||||
</div>
|
||||
<div id="privacyPolicy" style="display:none">
|
||||
<h2>Privacy Policy</h2>
|
||||
<p>This HTML5 Speedtest server is configured with telemetry enabled.</p>
|
||||
<h4>What data we collect</h4>
|
||||
<p>
|
||||
At the end of the test, the following data is collected and stored:
|
||||
<ul>
|
||||
<li>Test ID</li>
|
||||
<li>Time of testing</li>
|
||||
<li>Test results (download and upload speed, ping and jitter)</li>
|
||||
<li>IP address</li>
|
||||
<li>ISP information</li>
|
||||
<li>Approximate location (inferred from IP address, not GPS)</li>
|
||||
<li>User agent and browser locale</li>
|
||||
<li>Test log (contains no personal information)</li>
|
||||
</ul>
|
||||
</p>
|
||||
<h4>How we use the data</h4>
|
||||
<p>
|
||||
Data collected through this service is used to:
|
||||
<ul>
|
||||
<li>Allow sharing of test results (sharable image for forums, etc.)</li>
|
||||
<li>To improve the service offered to you (for instance, to detect problems on our side)</li>
|
||||
</ul>
|
||||
No personal information is disclosed to third parties.
|
||||
</p>
|
||||
<h4>Your consent</h4>
|
||||
<p>
|
||||
By starting the test, you consent to the terms of this privacy policy.
|
||||
</p>
|
||||
<h4>Data removal</h4>
|
||||
<p>
|
||||
If you want to have your information deleted, you need to provide either the ID of the test or your IP address. This is the only way to identify your data, without this information we won't be able to comply with your request.<br/><br/>
|
||||
Contact this email address for all deletion requests: <a href="mailto:PUT@YOUR_EMAIL.HERE">TO BE FILLED BY DEVELOPER</a>.
|
||||
</p>
|
||||
<br/><br/>
|
||||
<div class="closePrivacyPolicy">
|
||||
<a class="privacy" href="#" onclick="I('privacyPolicy').style.display='none'">Close</a>
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
<script type="text/javascript">setTimeout(function(){initUI()},100);</script>
|
||||
<p>Loading...</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
/**
|
||||
* Design by fromScratch Studio - 2022, 2023 (fromscratch.io)
|
||||
* Implementation in HTML/CSS/JS by Timendus - 2024 (https://github.com/Timendus)
|
||||
*
|
||||
* See https://github.com/librespeed/speedtest/issues/585
|
||||
*/
|
||||
|
||||
// States the UI can be in
|
||||
const INITIALIZING = 0;
|
||||
const READY = 1;
|
||||
const RUNNING = 2;
|
||||
const FINISHED = 3;
|
||||
|
||||
// Keep some global state here
|
||||
const testState = {
|
||||
state: INITIALIZING,
|
||||
speedtest: null,
|
||||
servers: [],
|
||||
selectedServerDirty: false,
|
||||
testData: null,
|
||||
testDataDirty: false,
|
||||
telemetryEnabled: false,
|
||||
};
|
||||
|
||||
// Bootstrap the application when the DOM is ready
|
||||
window.addEventListener("DOMContentLoaded", async () => {
|
||||
createSpeedtest();
|
||||
hookUpButtons();
|
||||
startRenderingLoop();
|
||||
applySettingsJSON();
|
||||
applyServerListJSON();
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a new Speedtest and hook it into the global state
|
||||
*/
|
||||
function createSpeedtest() {
|
||||
testState.speedtest = new Speedtest();
|
||||
testState.speedtest.onupdate = (data) => {
|
||||
testState.testData = data;
|
||||
testState.testDataDirty = true;
|
||||
};
|
||||
testState.speedtest.onend = (aborted) =>
|
||||
(testState.state = aborted ? READY : FINISHED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make all the buttons respond to the right clicks
|
||||
*/
|
||||
function hookUpButtons() {
|
||||
document
|
||||
.querySelector("#start-button")
|
||||
.addEventListener("click", startButtonClickHandler);
|
||||
document
|
||||
.querySelector("#choose-privacy")
|
||||
.addEventListener("click", () =>
|
||||
document.querySelector("#privacy").showModal()
|
||||
);
|
||||
document
|
||||
.querySelector("#share-results")
|
||||
.addEventListener("click", () =>
|
||||
document.querySelector("#share").showModal()
|
||||
);
|
||||
document
|
||||
.querySelector("#copy-link")
|
||||
.addEventListener("click", copyLinkButtonClickHandler);
|
||||
document
|
||||
.querySelectorAll(".close-dialog, #close-privacy")
|
||||
.forEach((element) => {
|
||||
element.addEventListener("click", () =>
|
||||
document.querySelectorAll("dialog").forEach((modal) => modal.close())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for clicks on the main start button
|
||||
*/
|
||||
function startButtonClickHandler() {
|
||||
switch (testState.state) {
|
||||
case READY:
|
||||
case FINISHED:
|
||||
testState.speedtest.start();
|
||||
testState.state = RUNNING;
|
||||
return;
|
||||
case RUNNING:
|
||||
testState.speedtest.abort();
|
||||
// testState.state is updated by `onend` handler of speedtest
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for clicks on the "Copy link" button in the modal
|
||||
*/
|
||||
async function copyLinkButtonClickHandler() {
|
||||
const link = document.querySelector("img#results").src;
|
||||
await navigator.clipboard.writeText(link);
|
||||
const button = document.querySelector("#copy-link");
|
||||
button.classList.add("active");
|
||||
button.textContent = "Copied!";
|
||||
setTimeout(() => {
|
||||
button.classList.remove("active");
|
||||
button.textContent = "Copy link";
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load settings from settings.json on the server and apply them
|
||||
*/
|
||||
async function applySettingsJSON() {
|
||||
try {
|
||||
const response = await fetch("settings.json");
|
||||
const settings = await response.json();
|
||||
if (!settings || typeof settings !== "object") {
|
||||
return console.error("Settings are empty or malformed");
|
||||
}
|
||||
for (let setting in settings) {
|
||||
testState.speedtest.setParameter(setting, settings[setting]);
|
||||
if (
|
||||
setting == "telemetry_level" &&
|
||||
settings[setting] &&
|
||||
settings[setting] != "off" &&
|
||||
settings[setting] != "disabled" &&
|
||||
settings[setting] != "false"
|
||||
) {
|
||||
testState.telemetryEnabled = true;
|
||||
document.querySelector("#privacy-warning").classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch settings:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load server list from the configured source and populate the dropdown
|
||||
*/
|
||||
async function applyServerListJSON() {
|
||||
try {
|
||||
const serverSource =
|
||||
typeof globalThis.SPEEDTEST_SERVERS !== "undefined"
|
||||
? globalThis.SPEEDTEST_SERVERS
|
||||
: "server-list.json";
|
||||
const servers = Array.isArray(serverSource)
|
||||
? serverSource
|
||||
: await fetch(serverSource).then((response) => response.json());
|
||||
if (!servers || !Array.isArray(servers) || servers.length === 0) {
|
||||
return console.error("Server list is empty or malformed");
|
||||
}
|
||||
|
||||
testState.servers = servers;
|
||||
|
||||
// If there's only one server, just show it. No reachability checks needed.
|
||||
if (servers.length === 1) {
|
||||
populateDropdown(servers);
|
||||
return;
|
||||
}
|
||||
|
||||
// For multiple servers: first run the built-in selection (which pings servers
|
||||
// and annotates them with pingT). Only then populate the dropdown so that
|
||||
// dead servers don't appear.
|
||||
testState.speedtest.addTestPoints(servers);
|
||||
testState.speedtest.selectServer((bestServer) => {
|
||||
const aliveServers = testState.servers.filter((s) => {
|
||||
// Keep servers that responded to ping (pingT !== -1).
|
||||
if (s.pingT !== -1) return true;
|
||||
// Also keep protocol-relative servers ("//...") as a defensive fallback.
|
||||
// LibreSpeed normalizes them to the page protocol before pinging, so they
|
||||
// are normally treated like any other server and get a real pingT value.
|
||||
return typeof s.server === "string" && s.server.startsWith("//");
|
||||
});
|
||||
|
||||
// Prefer to show only reachable servers, but if none are reachable,
|
||||
// fall back to the full list so users can still pick a server manually.
|
||||
if (aliveServers.length > 0) {
|
||||
testState.servers = aliveServers;
|
||||
}
|
||||
populateDropdown(testState.servers);
|
||||
|
||||
|
||||
if (bestServer) {
|
||||
selectServer(bestServer);
|
||||
} else {
|
||||
alert(
|
||||
"Can't reach any of the speedtest servers! But you're on this page. Something weird is going on with your network."
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to load server list:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add all the servers to the server selection dropdown and make it actually
|
||||
* work.
|
||||
* @param {Array} servers - an array of server objects
|
||||
*/
|
||||
function populateDropdown(servers) {
|
||||
const serverSelector = document.querySelector("div.server-selector");
|
||||
const serverList = serverSelector.querySelector("ul.servers");
|
||||
|
||||
// Reset previous state (populateDropdown can be called multiple times)
|
||||
serverSelector.classList.remove("single-server");
|
||||
serverSelector.classList.remove("active");
|
||||
serverList.classList.remove("active");
|
||||
serverList.innerHTML = "";
|
||||
|
||||
// If we have only a single server, just show it
|
||||
if (servers.length === 1) {
|
||||
serverSelector.classList.add("single-server");
|
||||
selectServer(servers[0]);
|
||||
return;
|
||||
}
|
||||
serverSelector.classList.add("active");
|
||||
|
||||
// Make the dropdown open and close (hook only once)
|
||||
if (serverSelector.dataset.hooked !== "1") {
|
||||
serverSelector.dataset.hooked = "1";
|
||||
|
||||
serverSelector.addEventListener("click", () => {
|
||||
serverList.classList.toggle("active");
|
||||
});
|
||||
document.addEventListener("click", (e) => {
|
||||
if (e.target.closest("div.server-selector") !== serverSelector)
|
||||
serverList.classList.remove("active");
|
||||
});
|
||||
}
|
||||
|
||||
// Populate the list to choose from
|
||||
servers.forEach((server) => {
|
||||
const item = document.createElement("li");
|
||||
const link = document.createElement("a");
|
||||
link.href = "#";
|
||||
link.innerHTML = `${server.name}${
|
||||
server.sponsorName ? ` <span>(${server.sponsorName})</span>` : ""
|
||||
}`;
|
||||
link.addEventListener("click", () => selectServer(server));
|
||||
item.appendChild(link);
|
||||
serverList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the given server as the selected server for the speedtest
|
||||
* @param {Object} server - a server object
|
||||
*/
|
||||
function selectServer(server) {
|
||||
testState.speedtest.setSelectedServer(server);
|
||||
testState.selectedServerDirty = true;
|
||||
testState.state = READY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the requestAnimationFrame UI rendering loop
|
||||
*/
|
||||
function startRenderingLoop() {
|
||||
// Do these queries once to speed up the rendering itself
|
||||
const serverSelector = document.querySelector("div.server-selector");
|
||||
const selectedServer = serverSelector.querySelector("#selected-server");
|
||||
const sponsor = serverSelector.querySelector("#sponsor");
|
||||
const startButton = document.querySelector("#start-button");
|
||||
const privacyWarning = document.querySelector("#privacy-warning");
|
||||
|
||||
const gauges = document.querySelectorAll("#download-gauge, #upload-gauge");
|
||||
const downloadProgress = document.querySelector("#download-gauge .progress");
|
||||
const uploadProgress = document.querySelector("#upload-gauge .progress");
|
||||
const downloadGauge = document.querySelector("#download-gauge .speed");
|
||||
const uploadGauge = document.querySelector("#upload-gauge .speed");
|
||||
const downloadText = document.querySelector("#download-gauge span");
|
||||
const uploadText = document.querySelector("#upload-gauge span");
|
||||
|
||||
const pingAndJitter = document.querySelectorAll(".ping, .jitter");
|
||||
const ping = document.querySelector("#ping");
|
||||
const jitter = document.querySelector("#jitter");
|
||||
const shareResults = document.querySelector("#share-results");
|
||||
const copyLink = document.querySelector("#copy-link");
|
||||
const resultsImage = document.querySelector("#results");
|
||||
|
||||
const buttonTexts = {
|
||||
[INITIALIZING]: "Loading...",
|
||||
[READY]: "Let's start",
|
||||
[RUNNING]: "Abort",
|
||||
[FINISHED]: "Restart",
|
||||
};
|
||||
|
||||
// Show copy link button only if navigator.clipboard is available
|
||||
copyLink.classList.toggle("hidden", !navigator.clipboard);
|
||||
|
||||
function renderUI() {
|
||||
// Make the main button reflect the current state
|
||||
startButton.textContent = buttonTexts[testState.state];
|
||||
startButton.classList.toggle("disabled", testState.state === INITIALIZING);
|
||||
startButton.classList.toggle("active", testState.state === RUNNING);
|
||||
|
||||
// Disable the server selector while test is running
|
||||
serverSelector.classList.toggle("disabled", testState.state === RUNNING);
|
||||
|
||||
// Show selected server
|
||||
if (testState.selectedServerDirty) {
|
||||
const server = testState.speedtest.getSelectedServer();
|
||||
selectedServer.textContent = server.name;
|
||||
if (server.sponsorName) {
|
||||
if (server.sponsorURL) {
|
||||
sponsor.innerHTML = `Sponsor: <a href="${server.sponsorURL}">${server.sponsorName}</a>`;
|
||||
} else {
|
||||
sponsor.textContent = `Sponsor: ${server.sponsorName}`;
|
||||
}
|
||||
} else {
|
||||
sponsor.innerHTML = " ";
|
||||
}
|
||||
testState.selectedServerDirty = false;
|
||||
}
|
||||
|
||||
// Activate the gauges when test running or finished
|
||||
gauges.forEach((e) =>
|
||||
e.classList.toggle(
|
||||
"enabled",
|
||||
testState.state === RUNNING || testState.state === FINISHED
|
||||
)
|
||||
);
|
||||
|
||||
// Show ping and jitter if data is available
|
||||
pingAndJitter.forEach((e) =>
|
||||
e.classList.toggle(
|
||||
"hidden",
|
||||
!(
|
||||
testState.testData &&
|
||||
testState.testData.pingStatus &&
|
||||
testState.testData.jitterStatus
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Show share button after test if server supports it
|
||||
shareResults.classList.toggle(
|
||||
"hidden",
|
||||
!(
|
||||
testState.state === FINISHED &&
|
||||
testState.telemetryEnabled &&
|
||||
testState.testData.testId
|
||||
)
|
||||
);
|
||||
|
||||
if (testState.testDataDirty) {
|
||||
// Set gauge rotations
|
||||
downloadProgress.style = `--progress-rotation: ${
|
||||
testState.testData.dlProgress * 180
|
||||
}deg`;
|
||||
uploadProgress.style = `--progress-rotation: ${
|
||||
testState.testData.ulProgress * 180
|
||||
}deg`;
|
||||
downloadGauge.style = `--speed-rotation: ${mbpsToRotation(
|
||||
testState.testData.dlStatus,
|
||||
testState.testData.testState === 1
|
||||
)}deg`;
|
||||
uploadGauge.style = `--speed-rotation: ${mbpsToRotation(
|
||||
testState.testData.ulStatus,
|
||||
testState.testData.testState === 3
|
||||
)}deg`;
|
||||
|
||||
// Set numeric values
|
||||
downloadText.textContent = numberToText(testState.testData.dlStatus);
|
||||
uploadText.textContent = numberToText(testState.testData.ulStatus);
|
||||
ping.textContent = numberToText(testState.testData.pingStatus);
|
||||
jitter.textContent = numberToText(testState.testData.jitterStatus);
|
||||
|
||||
// Set user's IP and provider
|
||||
if (testState.testData.clientIp) {
|
||||
// Clear previous content
|
||||
privacyWarning.innerHTML = '';
|
||||
|
||||
const connectedThrough = document.createElement('span');
|
||||
connectedThrough.textContent = 'You are connected through:';
|
||||
|
||||
const ipAddress = document.createTextNode(testState.testData.clientIp);
|
||||
|
||||
privacyWarning.appendChild(connectedThrough);
|
||||
privacyWarning.appendChild(document.createElement('br'));
|
||||
privacyWarning.appendChild(ipAddress);
|
||||
|
||||
privacyWarning.classList.remove("hidden");
|
||||
}
|
||||
|
||||
// Set image for sharing results
|
||||
if (testState.testData.testId) {
|
||||
resultsImage.src =
|
||||
window.location.href.substring(
|
||||
0,
|
||||
window.location.href.lastIndexOf("/")
|
||||
) +
|
||||
"/results/?id=" +
|
||||
testState.testData.testId;
|
||||
}
|
||||
|
||||
testState.testDataDirty = false;
|
||||
}
|
||||
|
||||
requestAnimationFrame(renderUI);
|
||||
}
|
||||
|
||||
renderUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a speed in Mbits per second to a rotation for the gauge
|
||||
* @param {string} speed Speed in Mbits
|
||||
* @param {boolean} oscillate If the gauge should wiggle a bit
|
||||
* @returns {number} Rotation for the gauge in degrees
|
||||
*/
|
||||
function mbpsToRotation(speed, oscillate) {
|
||||
speed = Number(speed);
|
||||
if (speed <= 0) return 0;
|
||||
|
||||
const minSpeed = 0;
|
||||
const maxSpeed = 10000; // 10 Gbps maxes out the gauge
|
||||
const minRotation = 0;
|
||||
const maxRotation = 180;
|
||||
|
||||
// Can't do log10 of values less than one, +1 all to keep it fair
|
||||
const logMinSpeed = Math.log10(minSpeed + 1);
|
||||
const logMaxSpeed = Math.log10(maxSpeed + 1);
|
||||
const logSpeed = Math.log10(speed + 1);
|
||||
|
||||
const power = (logSpeed - logMinSpeed) / (logMaxSpeed - logMinSpeed);
|
||||
const oscillation = oscillate ? 1 + 0.01 * Math.sin(Date.now() / 100) : 1;
|
||||
const rotation = power * oscillation * maxRotation;
|
||||
|
||||
// Make sure we stay within bounds at all times
|
||||
return Math.max(Math.min(rotation, maxRotation), minRotation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a number to a user friendly version
|
||||
* @param {string} value Speed, ping or jitter
|
||||
* @returns {string} A text version with proper decimals
|
||||
*/
|
||||
function numberToText(value) {
|
||||
if (!value) return "00";
|
||||
value = Number(value);
|
||||
if (value < 10) return value.toFixed(2);
|
||||
if (value < 100) return value.toFixed(1);
|
||||
return value.toFixed(0);
|
||||
}
|
||||
+44
-44
@@ -6,15 +6,15 @@
|
||||
*/
|
||||
|
||||
/*
|
||||
This is the main interface between your webpage and the speedtest.
|
||||
It hides the speedtest web worker to the page, and provides many convenient functions to control the test.
|
||||
|
||||
This is the main interface between your webpage and the speed test.
|
||||
It hides the speed test web worker to the page, and provides many convenient functions to control the test.
|
||||
|
||||
The best way to learn how to use this is to look at the basic example, but here's some documentation.
|
||||
|
||||
|
||||
To initialize the test, create a new Speedtest object:
|
||||
var s=new Speedtest();
|
||||
let s=new Speedtest();
|
||||
Now you can think of this as a finite state machine. These are the states (use getState() to see them):
|
||||
- 0: here you can change the speedtest settings (such as test duration) with the setParameter("parameter",value) method. From here you can either start the test using start() (goes to state 3) or you can add multiple test points using addTestPoint(server) or addTestPoints(serverList) (goes to state 1). Additionally, this is the perfect moment to set up callbacks for the onupdate(data) and onend(aborted) events.
|
||||
- 0: here you can change the speed test settings (such as test duration) with the setParameter("parameter",value) method. From here you can either start the test using start() (goes to state 3) or you can add multiple test points using addTestPoint(server) or addTestPoints(serverList) (goes to state 1). Additionally, this is the perfect moment to set up callbacks for the onupdate(data) and onend(aborted) events.
|
||||
- 1: here you can add test points. You only need to do this if you want to use multiple test points.
|
||||
A server is defined as an object like this:
|
||||
{
|
||||
@@ -27,16 +27,16 @@
|
||||
}
|
||||
While in state 1, you can only add test points, you cannot change the test settings. When you're done, use selectServer(callback) to select the test point with the lowest ping. This is asynchronous, when it's done, it will call your callback function and move to state 2. Calling setSelectedServer(server) will manually select a server and move to state 2.
|
||||
- 2: test point selected, ready to start the test. Use start() to begin, this will move to state 3
|
||||
- 3: test running. Here, your onupdate event calback will be called periodically, with data coming from the worker about speed and progress. A data object will be passed to your onupdate function, with the following items:
|
||||
- dlStatus: download speed in mbps
|
||||
- ulStatus: upload speed in mbps
|
||||
- 3: test running. Here, your onupdate event callback will be called periodically, with data coming from the worker about speed and progress. A data object will be passed to your onupdate function, with the following items:
|
||||
- dlStatus: download speed in Mbit/s
|
||||
- ulStatus: upload speed in Mbit/s
|
||||
- pingStatus: ping in ms
|
||||
- jitterStatus: jitter in ms
|
||||
- dlProgress: progress of the download test as a float 0-1
|
||||
- ulProgress: progress of the upload test as a float 0-1
|
||||
- pingProgress: progress of the ping/jitter test as a float 0-1
|
||||
- testState: state of the test (-1=not started, 0=starting, 1=download test, 2=ping+jitter test, 3=upload test, 4=finished, 5=aborted)
|
||||
- clientIp: IP address of the client performing the test (and optionally ISP and distance)
|
||||
- clientIp: IP address of the client performing the test (and optionally ISP and distance)
|
||||
At the end of the test, the onend function will be called, with a boolean specifying whether the test was aborted or if it ended normally.
|
||||
The test can be aborted at any time with abort().
|
||||
At the end of the test, it will move to state 4
|
||||
@@ -46,10 +46,10 @@
|
||||
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._settings = {}; //settings for the speed test worker
|
||||
this._state = 0; //0=adding settings, 1=adding servers, 2=server selection done, 3=test running, 4=done
|
||||
console.log(
|
||||
"LibreSpeed by Federico Dossena v5.2.4 - https://github.com/librespeed/speedtest"
|
||||
"LibreSpeed by Federico Dossena v6.1.0 - https://github.com/librespeed/speedtest"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ Speedtest.prototype = {
|
||||
* - 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.
|
||||
* Invalid values or nonexistant parameters will be ignored by the speed test worker.
|
||||
*/
|
||||
setParameter: function(parameter, value) {
|
||||
if (this._state == 3)
|
||||
@@ -125,7 +125,7 @@ Speedtest.prototype = {
|
||||
* 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]);
|
||||
for (let i = 0; i < list.length; i++) this.addTestPoint(list[i]);
|
||||
},
|
||||
/**
|
||||
* Load a JSON server list from URL (multiple points of test)
|
||||
@@ -144,11 +144,11 @@ Speedtest.prototype = {
|
||||
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();
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.onload = function(){
|
||||
try{
|
||||
var servers=JSON.parse(xhr.responseText);
|
||||
for(var i=0;i<servers.length;i++){
|
||||
const servers=JSON.parse(xhr.responseText);
|
||||
for(let i=0;i<servers.length;i++){
|
||||
this._checkServerDefinition(servers[i]);
|
||||
}
|
||||
this.addTestPoints(servers);
|
||||
@@ -193,27 +193,27 @@ Speedtest.prototype = {
|
||||
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) {
|
||||
const 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
|
||||
const PING_TIMEOUT = 2000;
|
||||
let 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) {
|
||||
const ping = function(url, rtt) {
|
||||
url += (url.match(/\?/) ? "&" : "?") + "cors=true";
|
||||
var xhr = new XMLHttpRequest();
|
||||
var t = new Date().getTime();
|
||||
let xhr = new XMLHttpRequest();
|
||||
let 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
|
||||
let instspd = new Date().getTime() - t; //rough timing estimate
|
||||
try {
|
||||
//try to get more accurate timing using performance API
|
||||
var p = performance.getEntriesByName(url);
|
||||
let p = performance.getEntriesByName(url);
|
||||
p = p[p.length - 1];
|
||||
var d = p.responseStart - p.requestStart;
|
||||
let d = p.responseStart - p.requestStart;
|
||||
if (d <= 0) d = p.duration;
|
||||
if (d > 0 && d < instspd) instspd = d;
|
||||
} catch (e) {}
|
||||
@@ -234,14 +234,14 @@ Speedtest.prototype = {
|
||||
}.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...
|
||||
const 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;
|
||||
const checkServer = function(server, done) {
|
||||
let i = 0;
|
||||
server.pingT = -1;
|
||||
if (server.server.indexOf(location.protocol) == -1) done();
|
||||
else {
|
||||
var nextPing = function() {
|
||||
const nextPing = function() {
|
||||
if (i++ == PINGS) {
|
||||
done();
|
||||
return;
|
||||
@@ -261,10 +261,10 @@ Speedtest.prototype = {
|
||||
}
|
||||
}.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++) {
|
||||
let i = 0;
|
||||
const done = function() {
|
||||
let bestServer = null;
|
||||
for (let i = 0; i < serverList.length; i++) {
|
||||
if (
|
||||
serverList[i].pingT != -1 &&
|
||||
(bestServer == null || serverList[i].pingT < bestServer.pingT)
|
||||
@@ -273,7 +273,7 @@ Speedtest.prototype = {
|
||||
}
|
||||
selected(bestServer);
|
||||
}.bind(this);
|
||||
var nextServer = function() {
|
||||
const nextServer = function() {
|
||||
if (i == serverList.length) {
|
||||
done();
|
||||
return;
|
||||
@@ -284,17 +284,17 @@ Speedtest.prototype = {
|
||||
}.bind(this);
|
||||
|
||||
//parallel server selection
|
||||
var CONCURRENCY = 6;
|
||||
var serverLists = [];
|
||||
for (var i = 0; i < CONCURRENCY; i++) {
|
||||
const CONCURRENCY = 6;
|
||||
let serverLists = [];
|
||||
for (let i = 0; i < CONCURRENCY; i++) {
|
||||
serverLists[i] = [];
|
||||
}
|
||||
for (var i = 0; i < this._serverList.length; i++) {
|
||||
for (let 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++) {
|
||||
let completed = 0;
|
||||
let bestServer = null;
|
||||
for (let i = 0; i < CONCURRENCY; i++) {
|
||||
select(
|
||||
serverLists[i],
|
||||
function(server) {
|
||||
@@ -323,14 +323,14 @@ Speedtest.prototype = {
|
||||
this.worker.onmessage = function(e) {
|
||||
if (e.data === this._prevData) return;
|
||||
else this._prevData = e.data;
|
||||
var data = JSON.parse(e.data);
|
||||
const 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);
|
||||
clearInterval(this.updater);
|
||||
this._state = 4;
|
||||
try {
|
||||
if (this.onend) this.onend(data.testState == 5);
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
*/
|
||||
|
||||
// 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)
|
||||
let testState = -1; // -1=not started, 0=starting, 1=download test, 2=ping+jitter test, 3=upload test, 4=finished, 5=abort
|
||||
let dlStatus = ""; // download speed in megabit/s with 2 decimal digits
|
||||
let ulStatus = ""; // upload speed in megabit/s with 2 decimal digits
|
||||
let pingStatus = ""; // ping in milliseconds with 2 decimal digits
|
||||
let jitterStatus = ""; // jitter in milliseconds with 2 decimal digits
|
||||
let clientIp = ""; // client's IP address as reported by getIP.php
|
||||
let dlProgress = 0; //progress of download test 0-1
|
||||
let ulProgress = 0; //progress of upload test 0-1
|
||||
let pingProgress = 0; //progress of ping+jitter test 0-1
|
||||
let testId = null; //test ID (sent back by telemetry if used, null otherwise)
|
||||
|
||||
var log = ""; //telemetry log
|
||||
let log = ""; //telemetry log
|
||||
function tlog(s) {
|
||||
if (settings.telemetry_level >= 2) {
|
||||
log += Date.now() + ": " + s + "\n";
|
||||
@@ -36,7 +36,7 @@ function twarn(s) {
|
||||
}
|
||||
|
||||
// test settings. can be overridden by sending specific values with the start command
|
||||
var settings = {
|
||||
let settings = {
|
||||
mpot: false, //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
|
||||
@@ -60,17 +60,17 @@ var settings = {
|
||||
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)
|
||||
overheadCompensationFactor: 1.06, //can be changed to compensate 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 and timing) 3=debug (results+log)
|
||||
url_telemetry: "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
|
||||
forceIE11Workaround: false //when set to true, it will force 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
|
||||
let xhr = null; // array of currently active xhr requests
|
||||
let interval = null; // timer used in tests
|
||||
let 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
|
||||
@@ -88,7 +88,7 @@ function url_sep(url) {
|
||||
example: start {"time_ul_max":"10", "time_dl_max":"10", "count_ping":"50"}
|
||||
*/
|
||||
this.addEventListener("message", function(e) {
|
||||
var params = e.data.split(" ");
|
||||
const params = e.data.split(" ");
|
||||
if (params[0] === "status") {
|
||||
// return status
|
||||
postMessage(
|
||||
@@ -111,19 +111,19 @@ this.addEventListener("message", function(e) {
|
||||
testState = 0;
|
||||
try {
|
||||
// parse settings, if present
|
||||
var s = {};
|
||||
let s = {};
|
||||
try {
|
||||
var ss = e.data.substring(5);
|
||||
const 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) {
|
||||
for (let key in s) {
|
||||
if (typeof settings[key] !== "undefined") settings[key] = s[key];
|
||||
else twarn("Unknown setting ignored: " + key);
|
||||
}
|
||||
var ua = navigator.userAgent;
|
||||
const 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)) {
|
||||
@@ -172,11 +172,11 @@ this.addEventListener("message", function(e) {
|
||||
// run the tests
|
||||
tverb(JSON.stringify(settings));
|
||||
test_pointer = 0;
|
||||
var iRun = false,
|
||||
let iRun = false,
|
||||
dRun = false,
|
||||
uRun = false,
|
||||
pRun = false;
|
||||
var runNextTest = function() {
|
||||
const runNextTest = function() {
|
||||
if (testState == 5) return;
|
||||
if (test_pointer >= settings.test_order.length) {
|
||||
//test is finished
|
||||
@@ -267,7 +267,7 @@ this.addEventListener("message", function(e) {
|
||||
function clearRequests() {
|
||||
tverb("stopping pending XHRs");
|
||||
if (xhr) {
|
||||
for (var i = 0; i < xhr.length; i++) {
|
||||
for (let i = 0; i < xhr.length; i++) {
|
||||
try {
|
||||
xhr[i].onprogress = null;
|
||||
xhr[i].onload = null;
|
||||
@@ -289,18 +289,18 @@ 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
|
||||
let ipCalled = false; // used to prevent multiple accidental calls to getIp
|
||||
let ispInfo = ""; //used for telemetry
|
||||
function getIp(done) {
|
||||
tverb("getIp");
|
||||
if (ipCalled) return;
|
||||
else ipCalled = true; // getIp already called?
|
||||
var startT = new Date().getTime();
|
||||
let 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);
|
||||
const data = JSON.parse(xhr.responseText);
|
||||
clientIp = data.processedString;
|
||||
ispInfo = data.rawIspInfo;
|
||||
} catch (e) {
|
||||
@@ -317,25 +317,25 @@ function getIp(done) {
|
||||
xhr.send();
|
||||
}
|
||||
// download test, calls done function when it's over
|
||||
var dlCalled = false; // used to prevent multiple accidental calls to dlTest
|
||||
let 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
|
||||
let 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) {
|
||||
const 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();
|
||||
let prevLoaded = 0; // number of bytes loaded last time onprogress was called
|
||||
let x = new XMLHttpRequest();
|
||||
xhr[i] = x;
|
||||
xhr[i].onprogress = function(event) {
|
||||
tverb("dl stream progress event " + i + " " + event.loaded);
|
||||
@@ -345,7 +345,7 @@ function dlTest(done) {
|
||||
} 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;
|
||||
const loadDiff = event.loaded <= 0 ? 0 : event.loaded - prevLoaded;
|
||||
if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return; // just in case
|
||||
totLoaded += loadDiff;
|
||||
prevLoaded = event.loaded;
|
||||
@@ -380,14 +380,14 @@ function dlTest(done) {
|
||||
);
|
||||
}.bind(this);
|
||||
// open streams
|
||||
for (var i = 0; i < settings.xhr_dlMultistream; i++) {
|
||||
for (let 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;
|
||||
const t = new Date().getTime() - startT;
|
||||
if (graceTimeDone) dlProgress = (t + bonusT) / (settings.time_dl_max * 1000);
|
||||
if (t < 200) return;
|
||||
if (!graceTimeDone) {
|
||||
@@ -401,10 +401,10 @@ function dlTest(done) {
|
||||
graceTimeDone = true;
|
||||
}
|
||||
} else {
|
||||
var speed = totLoaded / (t / 1000.0);
|
||||
const 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;
|
||||
const bonus = (5.0 * speed) / 100000;
|
||||
bonusT += bonus > 400 ? 400 : bonus;
|
||||
}
|
||||
//update status
|
||||
@@ -423,47 +423,47 @@ function dlTest(done) {
|
||||
200
|
||||
);
|
||||
}
|
||||
// upload test, calls done function whent it's over
|
||||
var ulCalled = false; // used to prevent multiple accidental calls to ulTest
|
||||
// upload test, calls done function when it's over
|
||||
let 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;
|
||||
let r = new ArrayBuffer(1048576);
|
||||
const maxInt = Math.pow(2, 32) - 1;
|
||||
try {
|
||||
r = new Uint32Array(r);
|
||||
for (var i = 0; i < r.length; i++) r[i] = Math.random() * maxInt;
|
||||
for (let 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);
|
||||
let req = [];
|
||||
let reqsmall = [];
|
||||
for (let 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;
|
||||
for (let 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
|
||||
const testFunction = function() {
|
||||
let 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) {
|
||||
const 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();
|
||||
let prevLoaded = 0; // number of bytes transmitted last time onprogress was called
|
||||
let x = new XMLHttpRequest();
|
||||
xhr[i] = x;
|
||||
var ie11workaround;
|
||||
let ie11workaround;
|
||||
if (settings.forceIE11Workaround) ie11workaround = true;
|
||||
else {
|
||||
try {
|
||||
@@ -474,7 +474,7 @@ function ulTest(done) {
|
||||
}
|
||||
}
|
||||
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
|
||||
// IE11 workaround: 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;
|
||||
@@ -496,7 +496,7 @@ function ulTest(done) {
|
||||
} 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;
|
||||
const loadDiff = event.loaded <= 0 ? 0 : event.loaded - prevLoaded;
|
||||
if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return; // just in case
|
||||
totLoaded += loadDiff;
|
||||
prevLoaded = event.loaded;
|
||||
@@ -528,14 +528,14 @@ function ulTest(done) {
|
||||
);
|
||||
}.bind(this);
|
||||
// open streams
|
||||
for (var i = 0; i < settings.xhr_ulMultistream; i++) {
|
||||
for (let 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;
|
||||
const t = new Date().getTime() - startT;
|
||||
if (graceTimeDone) ulProgress = (t + bonusT) / (settings.time_ul_max * 1000);
|
||||
if (t < 200) return;
|
||||
if (!graceTimeDone) {
|
||||
@@ -549,10 +549,10 @@ function ulTest(done) {
|
||||
graceTimeDone = true;
|
||||
}
|
||||
} else {
|
||||
var speed = totLoaded / (t / 1000.0);
|
||||
const 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;
|
||||
const bonus = (5.0 * speed) / 100000;
|
||||
bonusT += bonus > 400 ? 400 : bonus;
|
||||
}
|
||||
//update status
|
||||
@@ -584,20 +584,20 @@ function ulTest(done) {
|
||||
} else testFunction();
|
||||
}
|
||||
// ping+jitter test, function done is called when it's over
|
||||
var ptCalled = false; // used to prevent multiple accidental calls to pingTest
|
||||
let 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
|
||||
const startT = new Date().getTime(); //when the test was started
|
||||
let prevT = null; // last time a pong was received
|
||||
let ping = 0.0; // current ping value
|
||||
let jitter = 0.0; // current jitter value
|
||||
let i = 0; // counter of pongs received
|
||||
let prevInstspd = 0; // last ping time, used for jitter calculation
|
||||
xhr = [];
|
||||
// ping function
|
||||
var doPing = function() {
|
||||
const doPing = function() {
|
||||
tverb("ping");
|
||||
pingProgress = i / settings.count_ping;
|
||||
prevT = new Date().getTime();
|
||||
@@ -608,13 +608,13 @@ function pingTest(done) {
|
||||
if (i === 0) {
|
||||
prevT = new Date().getTime(); // first pong
|
||||
} else {
|
||||
var instspd = new Date().getTime() - prevT;
|
||||
let instspd = new Date().getTime() - prevT;
|
||||
if (settings.ping_allowPerformanceApi) {
|
||||
try {
|
||||
//try to get accurate performance timing using performance api
|
||||
var p = performance.getEntries();
|
||||
let p = performance.getEntries();
|
||||
p = p[p.length - 1];
|
||||
var d = p.responseStart - p.requestStart;
|
||||
let d = p.responseStart - p.requestStart;
|
||||
if (d <= 0) d = p.duration;
|
||||
if (d > 0 && d < instspd) instspd = d;
|
||||
} catch (e) {
|
||||
@@ -625,7 +625,7 @@ function pingTest(done) {
|
||||
//noticed that some browsers randomly have 0ms ping
|
||||
if (instspd < 1) instspd = prevInstspd;
|
||||
if (instspd < 1) instspd = 1;
|
||||
var instjitter = Math.abs(instspd - prevInstspd);
|
||||
const 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
|
||||
@@ -684,10 +684,10 @@ function sendTelemetry(done) {
|
||||
xhr = new XMLHttpRequest();
|
||||
xhr.onload = function() {
|
||||
try {
|
||||
var parts = xhr.responseText.split(" ");
|
||||
const parts = xhr.responseText.split(" ");
|
||||
if (parts[0] == "id") {
|
||||
try {
|
||||
var id = parts[1];
|
||||
let id = parts[1];
|
||||
done(id);
|
||||
} catch (e) {
|
||||
done(null);
|
||||
@@ -702,12 +702,12 @@ function sendTelemetry(done) {
|
||||
done(null);
|
||||
};
|
||||
xhr.open("POST", settings.url_telemetry + url_sep(settings.url_telemetry) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true);
|
||||
var telemetryIspInfo = {
|
||||
const telemetryIspInfo = {
|
||||
processedString: clientIp,
|
||||
rawIspInfo: typeof ispInfo === "object" ? ispInfo : ""
|
||||
};
|
||||
try {
|
||||
var fd = new FormData();
|
||||
const fd = new FormData();
|
||||
fd.append("ispinfo", JSON.stringify(telemetryIspInfo));
|
||||
fd.append("dl", dlStatus);
|
||||
fd.append("ul", ulStatus);
|
||||
@@ -717,7 +717,7 @@ function sendTelemetry(done) {
|
||||
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 : "");
|
||||
const 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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/* The main "start the test" button and the share button */
|
||||
|
||||
button {
|
||||
height: 6.8rem;
|
||||
min-width: 26.4rem;
|
||||
padding: 0 5rem;
|
||||
margin: 2.5rem;
|
||||
border-radius: 3.4rem;
|
||||
border: 0;
|
||||
|
||||
font-family: "Inter", sans-serif;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.1rem;
|
||||
color: var(--button-text-color);
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0.4rem 1.6rem 0 var(--button-shadow-color);
|
||||
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
transform: scale(1) translate3d(0, 0, 0) perspective(1px);
|
||||
|
||||
background: var(--button-gradient-1-color-1);
|
||||
transition: background-position 0.2s, transform 0.2s;
|
||||
background-position: 0% 0%;
|
||||
background: linear-gradient(
|
||||
92.97deg,
|
||||
var(--button-gradient-1-color-1) 0%,
|
||||
var(--button-gradient-1-color-1) 33%,
|
||||
var(--button-gradient-1-color-2) 40%,
|
||||
var(--button-gradient-1-color-3) 66.71%,
|
||||
var(--button-gradient-1-color-3) 100%
|
||||
);
|
||||
background-size: 300% 100%;
|
||||
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
transform: scale(1) translate3d(0, 0, 0) perspective(1px);
|
||||
background: var(--button-disabled-background-color);
|
||||
}
|
||||
&.small {
|
||||
height: 4.7rem;
|
||||
min-width: 20.2rem;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
&.inverted {
|
||||
border: 1px solid var(--button-gradient-1-color-1);
|
||||
color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
&:hover {
|
||||
background-position: 60% 0%;
|
||||
transform: scale(1.03) translate3d(0, 0, 0) perspective(1px);
|
||||
}
|
||||
&.active,
|
||||
&:active {
|
||||
background-position: 100% 0%;
|
||||
animation: pulse 0.7s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1.03) translate3d(0, 0, 0) perspective(1px);
|
||||
}
|
||||
20% {
|
||||
transform: scale(1.2) translate3d(0, 0, 0) perspective(1px);
|
||||
}
|
||||
40% {
|
||||
transform: scale(1) translate3d(0, 0, 0) perspective(1px);
|
||||
}
|
||||
60% {
|
||||
transform: scale(1.1) translate3d(0, 0, 0) perspective(1px);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1) translate3d(0, 0, 0) perspective(1px);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
:root {
|
||||
--theme-green: #5cf9fd;
|
||||
--theme-pink: #d63bc6;
|
||||
|
||||
--background-backup-color: #0e0720;
|
||||
--background-overlay-color: rgb(41 26 70 / 71%);
|
||||
|
||||
--primary-text-color: #ffffff;
|
||||
--tagline-text-color: var(--theme-green);
|
||||
--secondary-text-color: #898591;
|
||||
--primary-text-disabled-color: #888888;
|
||||
--secondary-text-disabled-color: #2e7d7f;
|
||||
|
||||
--button-text-color: #3e2f50;
|
||||
--button-gradient-1-color-1: #f5f5f5;
|
||||
--button-gradient-1-color-2: var(--theme-green);
|
||||
--button-gradient-1-color-3: var(--theme-pink);
|
||||
--button-shadow-color: #5cf9fd47;
|
||||
--button-disabled-background-color: #a2a2a2;
|
||||
|
||||
--server-selector-border-color: #625b6b;
|
||||
--server-selector-hover-border-color: var(--theme-green);
|
||||
--server-selector-background-color: #251b32;
|
||||
--server-selector-hover-background-color: var(--server-selector-border-color);
|
||||
|
||||
--gauge-background-color: #3e2f50;
|
||||
--gauge-progress-color: #726c7a;
|
||||
--gauge-pointer-green: #e2fbfc;
|
||||
--gauge-pointer-pink: #d091ca;
|
||||
|
||||
--ping-and-jitter-primary-text-color: #f5f5f5;
|
||||
--ping-and-jitter-secondary-text-color: #7b7b7b;
|
||||
|
||||
--popup-background-color: #251b32;
|
||||
--popup-shadow-color: #000000;
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/* Styling for the popups */
|
||||
|
||||
dialog {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 70vw;
|
||||
height: 70vh;
|
||||
margin: auto;
|
||||
margin-top: 23rem;
|
||||
|
||||
background: var(--popup-background-color);
|
||||
border: none;
|
||||
border-radius: 0.8rem;
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
max-width: 100vw; /* We need these overrides of browser defaults*/
|
||||
max-height: 100vh;
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
animation: fade-out 0.3s ease-out;
|
||||
&[open] {
|
||||
display: flex;
|
||||
animation: fade-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
& > .close-dialog {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
position: absolute;
|
||||
top: 3rem;
|
||||
right: 3rem;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& > section {
|
||||
max-width: 800px;
|
||||
overflow-y: auto;
|
||||
margin: 4rem 2rem 2rem 4rem;
|
||||
padding: 0 2rem 0 0;
|
||||
|
||||
& h1,
|
||||
& h2 {
|
||||
margin: 3rem 0 2rem 0;
|
||||
font-size: 3.6rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.2rem;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
& h2 {
|
||||
margin: 2rem 0 1rem 0;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
& p,
|
||||
& li {
|
||||
margin: 1rem 0 1rem 0;
|
||||
font-size: 1.6rem;
|
||||
line-height: 2.5rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.1rem;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
& ul {
|
||||
list-style-position: inside;
|
||||
margin: 1rem;
|
||||
|
||||
& li {
|
||||
margin: 0.1rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
& a {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.1rem;
|
||||
color: var(--secondary-text-color);
|
||||
text-underline-offset: 0.3rem;
|
||||
transition: text-underline-offset 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-green);
|
||||
text-underline-offset: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.6);
|
||||
display: none;
|
||||
}
|
||||
0.1% {
|
||||
display: flex;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
display: flex;
|
||||
}
|
||||
99.9% {
|
||||
display: flex;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.6);
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url(../fonts/Inter-latin-ext.woff2) format("woff2");
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
||||
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url(../fonts/Inter-latin.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Design by fromScratch Studio - 2022, 2023 (fromscratch.io)
|
||||
* Implementation in HTML/CSS/JS by Timendus - 2024 (https://github.com/Timendus)
|
||||
*
|
||||
* See https://github.com/librespeed/speedtest/issues/585
|
||||
*/
|
||||
|
||||
@import url("colors.css");
|
||||
@import url("fonts.css");
|
||||
@import url("main.css");
|
||||
@import url("server-selector.css");
|
||||
@import url("button.css");
|
||||
@import url("results.css");
|
||||
@import url("dialog.css");
|
||||
|
||||
/* Setting up the basic structure */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: var(--background-backup-color);
|
||||
background-image: url("../images/background.jpeg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
font-size: 10px;
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
font-size: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Inter", sans-serif;
|
||||
background-color: var(--background-overlay-color);
|
||||
color: var(--primary-text-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Position the logo */
|
||||
|
||||
header {
|
||||
padding: 4rem 7rem;
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
padding: 7rem 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Position the source code link */
|
||||
|
||||
footer {
|
||||
margin: auto auto 0 auto;
|
||||
padding: 5rem;
|
||||
|
||||
& > p.source a {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.1rem;
|
||||
color: var(--theme-green);
|
||||
text-underline-offset: 0.3rem;
|
||||
transition: text-underline-offset 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-pink);
|
||||
text-underline-offset: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
padding: 4rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/* Texts on the front page */
|
||||
|
||||
main {
|
||||
text-align: center;
|
||||
padding: 0 2rem;
|
||||
flex: 1;
|
||||
|
||||
& > h1 {
|
||||
margin: 0.2rem;
|
||||
font-size: 3.6rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.2rem;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
& p {
|
||||
margin-top: 8rem;
|
||||
font-size: 1.6rem;
|
||||
line-height: 2.5rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.1rem;
|
||||
color: var(--secondary-text-color);
|
||||
|
||||
&#privacy-warning {
|
||||
min-height: 5.3rem;
|
||||
|
||||
& > span {
|
||||
font-weight: 700;
|
||||
color: var(--theme-green);
|
||||
}
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > p.tagline {
|
||||
margin-top: 0;
|
||||
margin-bottom: 6rem;
|
||||
font-size: 2rem;
|
||||
color: var(--tagline-text-color);
|
||||
}
|
||||
|
||||
& a {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.1rem;
|
||||
color: var(--secondary-text-color);
|
||||
text-underline-offset: 0.3rem;
|
||||
transition: text-underline-offset 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-green);
|
||||
text-underline-offset: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/* Variables */
|
||||
|
||||
:root {
|
||||
--gauge-width: 32rem;
|
||||
--gauge-height: 22rem;
|
||||
--progress-width: 0.6rem;
|
||||
--speed-width: 3rem;
|
||||
}
|
||||
|
||||
/* Layout for the gauges */
|
||||
|
||||
.gauge-layout {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: start;
|
||||
justify-content: center;
|
||||
gap: 5rem;
|
||||
margin: 5rem auto 3rem auto;
|
||||
|
||||
@media screen and (max-width: 1100px) {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"download upload"
|
||||
"ping jitter";
|
||||
justify-items: center;
|
||||
justify-content: center;
|
||||
|
||||
--gauge-width: min(40vw, 32rem);
|
||||
--gauge-height: min(28vw, 22rem);
|
||||
--progress-width: min(1.2vw, 0.6rem);
|
||||
--speed-width: min(4vw, 3rem);
|
||||
}
|
||||
@media screen and (max-width: 500px) {
|
||||
gap: 5rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* The download/upload speed gauges */
|
||||
|
||||
/**
|
||||
* One thing I should really document here is the weird `transform: scale(1);`
|
||||
* and `position: fixed` in this code. This is a nasty little trick to allow the
|
||||
* gauge pointer to break out of the `overflow: hidden` of the .speed element.
|
||||
* We need the `overflow: hidden` to hide the arc that's rotating into view when
|
||||
* the value goes up. But we do want to see the full pointer, even when it's at
|
||||
* zero. This degrades fairly gracefully into showing half of the pointer when
|
||||
* browsers don't understand this.
|
||||
*
|
||||
* Trick taken from this article:
|
||||
* https://medium.com/@thomas.ryu/css-overriding-the-parents-overflow-hidden-90c75a0e7296
|
||||
*/
|
||||
|
||||
div.gauge {
|
||||
position: relative;
|
||||
transform: scale(1);
|
||||
width: var(--gauge-width);
|
||||
height: var(--gauge-height);
|
||||
|
||||
&.download {
|
||||
grid-area: download;
|
||||
}
|
||||
&.upload {
|
||||
grid-area: upload;
|
||||
}
|
||||
|
||||
& > .progress,
|
||||
& > .speed {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: var(--gauge-width);
|
||||
height: calc(var(--gauge-width) / 2);
|
||||
overflow: hidden;
|
||||
|
||||
&:after,
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
& > .progress {
|
||||
&:before,
|
||||
&:after {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: var(--gauge-width);
|
||||
height: calc(var(--gauge-width) / 2);
|
||||
|
||||
border-radius: 50% 50% 0 0 / 100% 100% 0 0;
|
||||
border: var(--progress-width) solid var(--gauge-background-color);
|
||||
border-bottom: 0;
|
||||
|
||||
transform-origin: bottom center;
|
||||
transform: rotate(var(--progress-rotation));
|
||||
transition: transform 0.2s linear;
|
||||
}
|
||||
&:after {
|
||||
top: calc(var(--gauge-width) / 2);
|
||||
|
||||
border-radius: 0 0 50% 50% / 0 0 100% 100%;
|
||||
border: var(--progress-width) solid var(--gauge-background-color);
|
||||
border-top: 0;
|
||||
|
||||
transform-origin: top center;
|
||||
}
|
||||
}
|
||||
|
||||
& > .speed {
|
||||
&:before,
|
||||
&:after {
|
||||
transform: rotate(var(--speed-rotation));
|
||||
transition: transform 0.2s ease;
|
||||
transition-timing-function: cubic-bezier(0.56, 0.04, 0.59, 0.91);
|
||||
}
|
||||
&:before {
|
||||
position: fixed;
|
||||
top: calc(var(--gauge-width) / 2 - var(--speed-width) / 3);
|
||||
left: var(--progress-width);
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
border-top: calc(var(--speed-width) / 3) solid transparent;
|
||||
border-bottom: calc(var(--speed-width) / 3) solid transparent;
|
||||
border-right: calc(var(--speed-width) * 0.97) solid
|
||||
var(--gauge-background-color);
|
||||
z-index: 1;
|
||||
|
||||
transform-origin: calc(var(--gauge-width) / 2 - var(--progress-width))
|
||||
calc(var(--speed-width) / 3);
|
||||
}
|
||||
&:after {
|
||||
top: calc(var(--gauge-width) / 2);
|
||||
left: calc(var(--progress-width) - 0.1rem);
|
||||
width: calc(var(--gauge-width) - var(--progress-width) * 2 + 0.2rem);
|
||||
height: calc(var(--gauge-width) / 2 - var(--progress-width) + 0.1rem);
|
||||
|
||||
border-radius: 0 0 50% 50% / 0 0 100% 100%;
|
||||
border: var(--speed-width) solid var(--gauge-background-color);
|
||||
border-top: 0;
|
||||
|
||||
transform-origin: top center;
|
||||
}
|
||||
}
|
||||
|
||||
&.enabled {
|
||||
&.download {
|
||||
& > .progress:after {
|
||||
border-color: var(--theme-pink);
|
||||
}
|
||||
& > .speed {
|
||||
&:before {
|
||||
border-right-color: var(--gauge-pointer-pink);
|
||||
}
|
||||
&:after {
|
||||
border-color: var(--theme-pink);
|
||||
}
|
||||
}
|
||||
}
|
||||
&.upload {
|
||||
& > .progress:after {
|
||||
border-color: var(--theme-green);
|
||||
}
|
||||
& > .speed {
|
||||
&:before {
|
||||
border-right-color: var(--gauge-pointer-green);
|
||||
}
|
||||
&:after {
|
||||
border-color: var(--theme-green);
|
||||
}
|
||||
}
|
||||
}
|
||||
& > h1 > span {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
& > h1,
|
||||
& > h2 {
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
font-family: "Inter", sans-serif;
|
||||
font-size: 2.1rem;
|
||||
letter-spacing: -0.1rem;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
& > h1 {
|
||||
bottom: calc(var(--gauge-height) - var(--gauge-width) / 2);
|
||||
font-weight: 300;
|
||||
|
||||
& > span {
|
||||
font-size: 5.5rem;
|
||||
font-weight: 200;
|
||||
display: block;
|
||||
color: var(--secondary-text-color);
|
||||
letter-spacing: -0.3rem;
|
||||
}
|
||||
}
|
||||
& > h2 {
|
||||
bottom: 0;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
& > h1 {
|
||||
font-size: 3vw;
|
||||
|
||||
& > span {
|
||||
font-size: 8vw;
|
||||
}
|
||||
}
|
||||
|
||||
& > h2 {
|
||||
font-size: 3vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Styling for Ping and Jitter */
|
||||
|
||||
.ping,
|
||||
.jitter {
|
||||
grid-area: jitter;
|
||||
display: flex;
|
||||
align-items: end;
|
||||
height: calc(var(--gauge-width) / 2);
|
||||
width: 13rem;
|
||||
|
||||
font-size: 2.1rem;
|
||||
letter-spacing: -0.1rem;
|
||||
font-weight: 300;
|
||||
color: var(--ping-and-jitter-secondary-text-color);
|
||||
|
||||
& > .label {
|
||||
font-weight: 700;
|
||||
}
|
||||
& > .value {
|
||||
color: var(--ping-and-jitter-primary-text-color);
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1100px) {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
justify-content: center !important;
|
||||
}
|
||||
@media screen and (max-width: 500px) {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
}
|
||||
.ping {
|
||||
grid-area: ping;
|
||||
justify-content: end;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/* The server selector fake dropdown */
|
||||
|
||||
.server-selector {
|
||||
position: relative;
|
||||
width: 50rem;
|
||||
margin: 0rem auto;
|
||||
display: none;
|
||||
|
||||
&.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& > .chosen {
|
||||
position: relative;
|
||||
height: 8.8rem;
|
||||
|
||||
border: 1px solid var(--server-selector-border-color);
|
||||
border-radius: 0.8rem;
|
||||
background-color: var(--server-selector-background-color);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--server-selector-hover-border-color);
|
||||
}
|
||||
|
||||
& > div.chevron {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
right: 1.8rem;
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
& > p {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
left: 2.4rem;
|
||||
top: 1.5rem;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.1rem;
|
||||
color: var(--theme-green);
|
||||
}
|
||||
|
||||
& > h2 {
|
||||
position: absolute;
|
||||
left: 2.4rem;
|
||||
right: 2.4rem;
|
||||
bottom: 1rem;
|
||||
|
||||
font-size: 2.4rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.2rem;
|
||||
color: var(--primary-text-color);
|
||||
text-align: left;
|
||||
text-transform: uppercase;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
& span {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Special case for when we have only one server */
|
||||
&.single-server {
|
||||
& > .chosen {
|
||||
cursor: default;
|
||||
&:hover {
|
||||
border-color: var(--server-selector-border-color);
|
||||
}
|
||||
& > div.chevron {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Overrides for when the test is running and the selector is disabled */
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
|
||||
& > .chosen {
|
||||
cursor: default;
|
||||
&:hover {
|
||||
border-color: var(--server-selector-border-color);
|
||||
}
|
||||
& > p {
|
||||
color: var(--secondary-text-disabled-color);
|
||||
}
|
||||
& > h2 {
|
||||
color: var(--primary-text-disabled-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Styling for the list of servers that pops out */
|
||||
& > ul.servers {
|
||||
position: absolute;
|
||||
width: 50rem;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
z-index: 1;
|
||||
|
||||
border: 1px solid var(--server-selector-border-color);
|
||||
border-radius: 0.8rem;
|
||||
background-color: var(--server-selector-background-color);
|
||||
list-style: none;
|
||||
|
||||
transform: scaleY(0);
|
||||
transform-origin: top;
|
||||
transition: transform 0.1s;
|
||||
|
||||
&.active {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& > li {
|
||||
&:first-child a {
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
&:last-child a {
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
& a {
|
||||
display: block;
|
||||
padding: 0.7rem 2.4rem;
|
||||
|
||||
font-size: 2.4rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.2rem;
|
||||
color: var(--sprint-text-color);
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
|
||||
transition: background-color 0.2s;
|
||||
|
||||
& span {
|
||||
font-weight: 400;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--server-selector-hover-background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Styling for the sponsor text under the dropdown */
|
||||
& > p.sponsor {
|
||||
margin: 1rem 0 5rem 0;
|
||||
|
||||
& a {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// normalizeCandidateIP validates and normalizes an IP address candidate
|
||||
// from a request header. It trims whitespace, takes the first comma-separated
|
||||
// token (for XFF-like headers that may contain a chain), and validates.
|
||||
func normalizeCandidateIP(raw string, ipv6 bool) string {
|
||||
ip := strings.TrimSpace(raw)
|
||||
// For XFF-like values, take the first address before a comma
|
||||
if idx := strings.Index(ip, ","); idx != -1 {
|
||||
ip = strings.TrimSpace(ip[:idx])
|
||||
}
|
||||
if ip == "" {
|
||||
return ""
|
||||
}
|
||||
if ipv6 {
|
||||
parsed := net.ParseIP(ip)
|
||||
if parsed != nil && parsed.To16() != nil && parsed.To4() == nil {
|
||||
return strings.TrimPrefix(ip, "::ffff:")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
parsed := net.ParseIP(ip)
|
||||
if parsed != nil {
|
||||
return strings.TrimPrefix(ip, "::ffff:")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getClientIP extracts the real client IP from the request using the following
|
||||
// priority chain, mirroring the PHP getIP_util.php behavior:
|
||||
//
|
||||
// 1. CF-Connecting-IPv6 (Cloudflare, must be a valid IPv6)
|
||||
// 2. Client-IP
|
||||
// 3. X-Real-IP
|
||||
// 4. X-Forwarded-For (first address in the chain)
|
||||
// 5. RemoteAddr (fallback)
|
||||
func getClientIP(r *http.Request) string {
|
||||
// 1. Cloudflare IPv6 header — must be a valid IPv6 address
|
||||
if cf := r.Header.Get("CF-Connecting-IPv6"); cf != "" {
|
||||
if ip := normalizeCandidateIP(cf, true); ip != "" {
|
||||
return strings.TrimPrefix(ip, "::ffff:")
|
||||
}
|
||||
}
|
||||
|
||||
// 2–4. Other forwarding / proxy headers — accept any valid IP
|
||||
for _, header := range []string{"Client-IP", "X-Real-IP", "X-Forwarded-For"} {
|
||||
if v := r.Header.Get(header); v != "" {
|
||||
if ip := normalizeCandidateIP(v, false); ip != "" {
|
||||
return strings.TrimPrefix(ip, "::ffff:")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Fallback: RemoteAddr set by the server
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
// RemoteAddr may not have a port in some environments
|
||||
ip = r.RemoteAddr
|
||||
}
|
||||
if parsed := net.ParseIP(ip); parsed != nil {
|
||||
return strings.TrimPrefix(ip, "::ffff:")
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// classifyPrivateIP returns a human-readable description if the IP is a
|
||||
// private or special-purpose address, or an empty string otherwise.
|
||||
// Mirrors the PHP getLocalOrPrivateIpInfo() function.
|
||||
func classifyPrivateIP(ip string) string {
|
||||
// Strip IPv4-mapped IPv6 prefix if present
|
||||
ip = strings.TrimPrefix(ip, "::ffff:")
|
||||
|
||||
switch {
|
||||
case ip == "::1":
|
||||
return "localhost IPv6 access"
|
||||
case strings.HasPrefix(ip, "fe80:"):
|
||||
return "link-local IPv6 access"
|
||||
// ULA IPv6 (fc00::/7): fc00:: - fdff:ffff:...
|
||||
case isULAIPv6(ip):
|
||||
return "ULA IPv6 access"
|
||||
case strings.HasPrefix(ip, "127."):
|
||||
return "localhost IPv4 access"
|
||||
case strings.HasPrefix(ip, "10."):
|
||||
return "private IPv4 access"
|
||||
case mustCompile(`^172\.(1[6-9]|2\d|3[01])\.`).MatchString(ip):
|
||||
return "private IPv4 access"
|
||||
case strings.HasPrefix(ip, "192.168"):
|
||||
return "private IPv4 access"
|
||||
case strings.HasPrefix(ip, "169.254"):
|
||||
return "link-local IPv4 access"
|
||||
case mustCompile(`^100\.([6-9][0-9]|1[0-2][0-7])\.`).MatchString(ip):
|
||||
return "CGNAT IPv4 access"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// isULAIPv6 checks if an IP is a Unique Local IPv6 Unicast Address (fc00::/7).
|
||||
func isULAIPv6(ipStr string) bool {
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil || ip.To16() == nil {
|
||||
return false
|
||||
}
|
||||
// fc00::/7 means the first 7 bits are 1111110
|
||||
// So the first byte & 0xFE must equal 0xFC
|
||||
return ip[0]&0xFE == 0xFC
|
||||
}
|
||||
|
||||
// mustCompile is a helper that compiles a regex and panics on error
|
||||
// (safe to use for static patterns).
|
||||
func mustCompile(pattern string) *regexp.Regexp {
|
||||
return regexp.MustCompile(pattern)
|
||||
}
|
||||
+138
-4
@@ -5,10 +5,13 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/oschwald/maxminddb-golang"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/umahmood/haversine"
|
||||
|
||||
@@ -140,16 +143,147 @@ func calculateDistance(clientLocation string, unit string) string {
|
||||
}
|
||||
|
||||
dist, km := haversine.Distance(clientCoord, serverCoord)
|
||||
unitString := " mi"
|
||||
|
||||
switch unit {
|
||||
case "km":
|
||||
dist = km
|
||||
unitString = " km"
|
||||
rounded := roundToNearest10(dist)
|
||||
if dist < 20 {
|
||||
return "<20 km"
|
||||
}
|
||||
return fmt.Sprintf("%.0f km", rounded)
|
||||
case "NM":
|
||||
dist = km * 0.539957
|
||||
unitString = " NM"
|
||||
return fmt.Sprintf("%.2f NM", dist)
|
||||
default: // miles
|
||||
distMi := dist
|
||||
rounded := roundToNearest10(distMi)
|
||||
if distMi < 15 {
|
||||
return "<15 mi"
|
||||
}
|
||||
return fmt.Sprintf("%.0f mi", rounded)
|
||||
}
|
||||
}
|
||||
|
||||
// roundToNearest10 rounds a float64 to the nearest 10, matching PHP round($d, -1)
|
||||
func roundToNearest10(val float64) float64 {
|
||||
return float64(int64(val/10+0.5)) * 10
|
||||
}
|
||||
|
||||
// GeoIP database holder (lazily opened on first use)
|
||||
var (
|
||||
geoIPReader *maxminddb.Reader
|
||||
geoIPOpened bool
|
||||
)
|
||||
|
||||
// getGeoIPData looks up the given IP in the configured GeoIP .mmdb database
|
||||
// and returns ISP and country information if available.
|
||||
// It returns nil if GeoIP is not configured or the lookup fails.
|
||||
func getGeoIPData(ipStr string) *struct {
|
||||
ASName string
|
||||
CountryName string
|
||||
} {
|
||||
conf := config.LoadedConfig()
|
||||
if conf.GeoIPDatabaseFile == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%.2f%s", dist, unitString)
|
||||
if !geoIPOpened {
|
||||
geoIPOpened = true
|
||||
if _, err := os.Stat(conf.GeoIPDatabaseFile); os.IsNotExist(err) {
|
||||
log.Warnf("GeoIP database file not found: %s", conf.GeoIPDatabaseFile)
|
||||
return nil
|
||||
}
|
||||
reader, err := maxminddb.Open(conf.GeoIPDatabaseFile)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to open GeoIP database: %s", err)
|
||||
return nil
|
||||
}
|
||||
geoIPReader = reader
|
||||
}
|
||||
|
||||
if geoIPReader == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try ipinfo.io offline database format first
|
||||
var ipinfoResult map[string]interface{}
|
||||
if err := geoIPReader.Lookup(ip, &ipinfoResult); err != nil {
|
||||
log.Warnf("GeoIP lookup failed: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(ipinfoResult) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := &struct {
|
||||
ASName string
|
||||
CountryName string
|
||||
}{}
|
||||
|
||||
// ipinfo.io offline format uses "as_name" and "country_name"
|
||||
if v, ok := ipinfoResult["as_name"].(string); ok {
|
||||
result.ASName = v
|
||||
}
|
||||
if v, ok := ipinfoResult["country_name"].(string); ok {
|
||||
result.CountryName = v
|
||||
}
|
||||
|
||||
// If ipinfo format fields are empty, try standard MaxMind GeoIP2 format
|
||||
if result.ASName == "" {
|
||||
// Try autonomous_system > organization
|
||||
if as, ok := ipinfoResult["autonomous_system"].(map[string]interface{}); ok {
|
||||
if v, ok := as["organization"].(string); ok {
|
||||
result.ASName = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if result.CountryName == "" {
|
||||
if country, ok := ipinfoResult["country"].(map[string]interface{}); ok {
|
||||
if v, ok := country["names"].(map[string]interface{}); ok {
|
||||
if n, ok := v["en"].(string); ok {
|
||||
result.CountryName = n
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: direct "country" string field (as used by some GeoIP DBs)
|
||||
if result.CountryName == "" {
|
||||
if v, ok := ipinfoResult["country"].(string); ok {
|
||||
result.CountryName = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if result.ASName == "" && result.CountryName == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// getISPInfoByPriority tries to fetch ISP info using the ipinfo.io API first,
|
||||
// then falls back to the configured offline GeoIP database, mirroring PHP behavior.
|
||||
func getISPInfoByPriority(addr string) results.IPInfoResponse {
|
||||
// First try: ipinfo.io API
|
||||
info := getIPInfo(addr)
|
||||
if info.Organization != "" || info.Country != "" {
|
||||
return info
|
||||
}
|
||||
|
||||
// Second try: offline GeoIP database
|
||||
geo := getGeoIPData(addr)
|
||||
if geo != nil {
|
||||
info.Organization = geo.ASName
|
||||
info.Country = geo.CountryName
|
||||
return info
|
||||
}
|
||||
|
||||
// Fallback: empty result (will show IP only)
|
||||
return info
|
||||
}
|
||||
|
||||
+25
-30
@@ -11,7 +11,6 @@ import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
@@ -79,6 +78,8 @@ func ListenAndServe(conf *config.Config) error {
|
||||
r.Post(conf.BaseURL+"/backend/results/telemetry", results.Record)
|
||||
r.HandleFunc(conf.BaseURL+"/stats", results.Stats)
|
||||
r.HandleFunc(conf.BaseURL+"/backend/stats", results.Stats)
|
||||
r.Get(conf.BaseURL+"/results/json", results.JSONResult)
|
||||
r.Get(conf.BaseURL+"/backend/results/json", results.JSONResult)
|
||||
|
||||
// PHP frontend default values compatibility
|
||||
r.HandleFunc(conf.BaseURL+"/empty.php", empty)
|
||||
@@ -91,6 +92,8 @@ func ListenAndServe(conf *config.Config) error {
|
||||
r.Post(conf.BaseURL+"/backend/results/telemetry.php", results.Record)
|
||||
r.HandleFunc(conf.BaseURL+"/stats.php", results.Stats)
|
||||
r.HandleFunc(conf.BaseURL+"/backend/stats.php", results.Stats)
|
||||
r.Get(conf.BaseURL+"/results/json.php", results.JSONResult)
|
||||
r.Get(conf.BaseURL+"/backend/results/json.php", results.JSONResult)
|
||||
|
||||
go listenProxyProtocol(conf, r)
|
||||
|
||||
@@ -132,6 +135,16 @@ func pages(fs http.FileSystem, BaseURL string) http.HandlerFunc {
|
||||
return fn
|
||||
}
|
||||
|
||||
// sendPHPCORSHeaders sets CORS headers matching the PHP backend's ?cors parameter behavior.
|
||||
// This is for API compatibility with the PHP version; the global CORS middleware already handles CORS.
|
||||
func sendPHPCORSHeaders(w http.ResponseWriter, r *http.Request) {
|
||||
if r.FormValue("cors") == "true" {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Encoding, Content-Type")
|
||||
}
|
||||
}
|
||||
|
||||
func empty(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := io.Copy(ioutil.Discard, r.Body)
|
||||
if err != nil {
|
||||
@@ -140,11 +153,13 @@ func empty(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
_ = r.Body.Close()
|
||||
|
||||
sendPHPCORSHeaders(w, r)
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func garbage(w http.ResponseWriter, r *http.Request) {
|
||||
sendPHPCORSHeaders(w, r)
|
||||
w.Header().Set("Content-Description", "File Transfer")
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=random.dat")
|
||||
@@ -180,37 +195,17 @@ func garbage(w http.ResponseWriter, r *http.Request) {
|
||||
func getIP(w http.ResponseWriter, r *http.Request) {
|
||||
var ret results.Result
|
||||
|
||||
clientIP := r.RemoteAddr
|
||||
clientIP = strings.ReplaceAll(clientIP, "::ffff:", "")
|
||||
clientIP := getClientIP(r)
|
||||
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err == nil {
|
||||
clientIP = ip
|
||||
}
|
||||
// Add anti-cache headers matching PHP behavior
|
||||
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0, s-maxage=0")
|
||||
w.Header().Add("Cache-Control", "post-check=0, pre-check=0")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
|
||||
isSpecialIP := true
|
||||
switch {
|
||||
case clientIP == "::1":
|
||||
ret.ProcessedString = clientIP + " - localhost IPv6 access"
|
||||
case strings.HasPrefix(clientIP, "fe80:"):
|
||||
ret.ProcessedString = clientIP + " - link-local IPv6 access"
|
||||
case strings.HasPrefix(clientIP, "127."):
|
||||
ret.ProcessedString = clientIP + " - localhost IPv4 access"
|
||||
case strings.HasPrefix(clientIP, "10."):
|
||||
ret.ProcessedString = clientIP + " - private IPv4 access"
|
||||
case regexp.MustCompile(`^172\.(1[6-9]|2\d|3[01])\.`).MatchString(clientIP):
|
||||
ret.ProcessedString = clientIP + " - private IPv4 access"
|
||||
case strings.HasPrefix(clientIP, "192.168"):
|
||||
ret.ProcessedString = clientIP + " - private IPv4 access"
|
||||
case strings.HasPrefix(clientIP, "169.254"):
|
||||
ret.ProcessedString = clientIP + " - link-local IPv4 access"
|
||||
case regexp.MustCompile(`^100\.([6-9][0-9]|1[0-2][0-7])\.`).MatchString(clientIP):
|
||||
ret.ProcessedString = clientIP + " - CGNAT IPv4 access"
|
||||
default:
|
||||
isSpecialIP = false
|
||||
}
|
||||
sendPHPCORSHeaders(w, r)
|
||||
|
||||
if isSpecialIP {
|
||||
if desc := classifyPrivateIP(clientIP); desc != "" {
|
||||
ret.ProcessedString = clientIP + " - " + desc
|
||||
b, _ := json.Marshal(&ret)
|
||||
if _, err := w.Write(b); err != nil {
|
||||
log.Errorf("Error writing to client: %s", err)
|
||||
@@ -224,7 +219,7 @@ func getIP(w http.ResponseWriter, r *http.Request) {
|
||||
ret.ProcessedString = clientIP
|
||||
|
||||
if getISPInfo {
|
||||
ispInfo := getIPInfo(clientIP)
|
||||
ispInfo := getISPInfoByPriority(clientIP)
|
||||
ret.RawISPInfo = ispInfo
|
||||
|
||||
removeRegexp := regexp.MustCompile(`AS\d+\s`)
|
||||
|
||||
Reference in New Issue
Block a user