5 Commits

Author SHA1 Message Date
64cf616e59 Add index.html 2020-06-17 13:35:15 +08:00
60329fc832 Add option to specify config file 2020-06-16 16:24:29 +08:00
bd721cdb07 Allow user to specify assets path in settings
Fixes #2
2020-06-16 16:24:29 +08:00
8971ba8117 Handle URLs with an extraneous /backend/ prefix (#1)
* handle /backend in URLs as speedtest_worker.js is set up to do so

* fixed dropped slashes
2020-06-08 11:43:51 +08:00
30c56a0d46 Add assets and settings.toml to release archive 2020-06-03 10:59:28 +08:00
9 changed files with 443 additions and 15 deletions

View File

@ -83,7 +83,10 @@ archives:
- goos: windows
format: zip
files:
- README.md
- LICENSE
- assets/*
- settings.toml
checksum:
name_template: 'checksums.txt'
changelog:

View File

@ -95,6 +95,9 @@ manually, you can install newer version of Go into your GOPATH:
server_lng=0
# ipinfo.io API key, if applicable
ipinfo_api_key=""
# assets directory path, defaults to `assets` in the same directory
assets_path="./assets"
# password for logging into statistics page, change this to enable stats page
statistics_password="PASSWORD"

365
assets/index.html Normal file
View File

@ -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
s.setParameter("url_telemetry", "results/telemetry");
var server = {
name: "demo",
server:window.location.protocol + "//" + window.location.host,
dlURL:"garbage",
ulURL:"empty",
pingURL:"empty",
getIpURL:"getIP",
};
s.setSelectedServer(server);
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;
display:block;
}
@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">Mbit/s</div>
</div>
<div class="testArea">
<div class="testName">Upload</div>
<canvas id="ulMeter" class="meter"></canvas>
<div id="ulText" class="meterText"></div>
<div class="unit">Mbit/s</div>
</div>
</div>
<div id="ipArea">
<span id="ip"></span>
</div>
<div id="shareArea" style="display:none">
<h3>Share results</h3>
<p>Test ID: <span id="testId"></span></p>
<input type="text" value="" id="resultsURL" readonly="readonly" onclick="this.select();this.focus();this.select();document.execCommand('copy');alert('Link copied')"/>
<img src="" id="resultsImg" />
</div>
</div>
<a href="https://github.com/librespeed/speedtest">Source code</a>
</div>
<div id="privacyPolicy" style="display:none">
<h2>Privacy Policy</h2>
<p>This HTML5 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/>
<a class="privacy" href="#" onclick="I('privacyPolicy').style.display='none'">Close</a><br/>
</div>
<script type="text/javascript">setTimeout(function(){initUI()},100);</script>
</body>
</html>

View File

@ -1,6 +1,8 @@
package config
import (
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
@ -15,6 +17,8 @@ type Config struct {
StatsPassword string `mapstructure:"statistics_password"`
RedactIP bool `mapstructure:"redact_ip_addresses"`
AssetsPath string `mapstructure:"assets_path"`
DatabaseType string `mapstructure:"database_type"`
DatabaseHostname string `mapstructure:"database_hostname"`
DatabaseName string `mapstructure:"database_name"`
@ -35,6 +39,7 @@ func init() {
viper.SetDefault("enable_cors", false)
viper.SetDefault("statistics_password", "PASSWORD")
viper.SetDefault("redact_ip_addresses", false)
viper.SetDefault("assets_path", "./assets")
viper.SetDefault("database_type", "postgresql")
viper.SetDefault("database_hostname", "localhost")
viper.SetDefault("database_name", "speedtest")
@ -50,7 +55,7 @@ func Load() Config {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
log.Warnf("No config file found in search paths, using default values")
} else {
log.Fatalf("Error reading config: %+v", err)
log.Fatalf("Error reading config: %s", err)
}
}
@ -63,6 +68,30 @@ func Load() Config {
return conf
}
func LoadFile(configFile string) Config {
var conf Config
f, err := os.OpenFile(configFile, os.O_RDONLY, 0444)
if err != nil {
log.Fatalf("Failed to open config file: %s", err)
}
defer f.Close()
if err := viper.ReadConfig(f); err != nil {
log.Fatalf("Error reading config: %s", err)
}
if err := viper.Unmarshal(&conf); err != nil {
log.Fatalf("Error parsing config: %s", err)
}
loadedConfig = &conf
return conf
}
func LoadedConfig() *Config {
if loadedConfig == nil {
Load()

20
main.go
View File

@ -1,16 +1,32 @@
package main
import (
"flag"
"github.com/librespeed/speedtest/config"
"github.com/librespeed/speedtest/database"
"github.com/librespeed/speedtest/results"
"github.com/librespeed/speedtest/web"
log "github.com/sirupsen/logrus"
)
func main() {
conf := config.Load()
var (
optConfig = flag.String("c", "", "config file to be used, defaults to settings.toml in the same directory")
)
func main() {
flag.Parse()
var conf config.Config
if *optConfig != "" {
conf = config.LoadFile(*optConfig)
} else {
conf = config.Load()
}
web.SetServerLocation(&conf)
results.Initialize(&conf)
database.SetDBInfo(&conf)
log.Fatal(web.ListenAndServe(&conf))
}

View File

@ -10,6 +10,7 @@ import (
"math/rand"
"net"
"net/http"
"path/filepath"
"regexp"
"strings"
"time"
@ -75,10 +76,10 @@ type IPInfoResponse struct {
Readme string `json:"readme"`
}
func init() {
func Initialize(c *config.Config) {
// changed to use Noto Sans instead of OpenSans, due to issue:
// https://github.com/golang/freetype/issues/8
if b, err := ioutil.ReadFile("assets/NotoSansDisplay-Light.ttf"); err != nil {
if b, err := ioutil.ReadFile(filepath.Join(c.AssetsPath, "NotoSansDisplay-Light.ttf")); err != nil {
log.Fatalf("Error opening NotoSansDisplay-Light font: %s", err)
} else {
f, err := freetype.ParseFont(b)
@ -88,7 +89,7 @@ func init() {
fontLight = f
}
if b, err := ioutil.ReadFile("assets/NotoSansDisplay-Medium.ttf"); err != nil {
if b, err := ioutil.ReadFile(filepath.Join(c.AssetsPath, "NotoSansDisplay-Medium.ttf")); err != nil {
log.Fatalf("Error opening NotoSansDisplay-Medium font: %s", err)
} else {
f, err := freetype.ParseFont(b)

View File

@ -8,6 +8,9 @@ server_lng=0
# ipinfo.io API key, if applicable
ipinfo_api_key=""
# assets directory path, defaults to `assets` in the same directory
assets_path="./assets"
# password for logging into statistics page
statistics_password="PASSWORD"
# redact IP addresses

View File

@ -17,11 +17,7 @@ import (
)
var (
// get server location from ipinfo.io from start to minimize API access
serverLat, serverLng = getServerLocation()
// for testing
// serverLat, serverLng = 22.7702, 112.9578
// serverLat, serverLng = 23.018, 113.7487
serverLat, serverLng float64
)
func getRandomData(length int) []byte {
@ -71,9 +67,7 @@ func getIPInfo(addr string) results.IPInfoResponse {
return ret
}
func getServerLocation() (float64, float64) {
conf := config.LoadedConfig()
func SetServerLocation(conf *config.Config) (float64, float64) {
if conf.ServerLat > 0 && conf.ServerLng > 0 {
log.Infof("Configured server coordinates: %.6f, %.6f", conf.ServerLat, conf.ServerLng)
return conf.ServerLat, conf.ServerLng

View File

@ -47,19 +47,31 @@ func ListenAndServe(conf *config.Config) error {
log.Infof("Starting backend server on %s", net.JoinHostPort(conf.BindAddress, conf.Port))
r.Get("/*", pages)
r.HandleFunc("/empty", empty)
r.HandleFunc("/backend/empty", empty)
r.Get("/garbage", garbage)
r.Get("/backend/garbage", garbage)
r.Get("/getIP", getIP)
r.Get("/backend/getIP", getIP)
r.Get("/results", results.DrawPNG)
r.Get("/results/", results.DrawPNG)
r.Get("/backend/results", results.DrawPNG)
r.Get("/backend/results/", results.DrawPNG)
r.Post("/results/telemetry", results.Record)
r.Post("/backend/results/telemetry", results.Record)
r.HandleFunc("/stats", results.Stats)
r.HandleFunc("/backend/stats", results.Stats)
// PHP frontend default values compatibility
r.HandleFunc("/empty.php", empty)
r.HandleFunc("/backend/empty.php", empty)
r.Get("/garbage.php", garbage)
r.Get("/backend/garbage.php", garbage)
r.Get("/getIP.php", getIP)
r.Get("/backend/getIP.php", getIP)
r.Post("/results/telemetry.php", results.Record)
r.Post("/backend/results/telemetry.php", results.Record)
r.HandleFunc("/stats.php", results.Stats)
r.HandleFunc("/backend/stats.php", results.Stats)
return http.ListenAndServe(net.JoinHostPort(conf.BindAddress, conf.Port), r)
}
@ -69,9 +81,11 @@ func pages(w http.ResponseWriter, r *http.Request) {
r.RequestURI = "/index.html"
}
conf := config.LoadedConfig()
uri := strings.Split(r.RequestURI, "?")[0]
if strings.HasSuffix(uri, ".html") || strings.HasSuffix(uri, ".js") {
http.FileServer(http.Dir("assets")).ServeHTTP(w, r)
http.FileServer(http.Dir(conf.AssetsPath)).ServeHTTP(w, r)
} else {
w.WriteHeader(http.StatusForbidden)
}