Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
abc8ac0a7b | |||
8ce11fa824 | |||
![]() |
7001fa4fa5 | ||
![]() |
fa28701928 | ||
![]() |
4afbe82283 | ||
![]() |
a3eec03845 | ||
![]() |
2ea9df0aba | ||
![]() |
8e31fe25ac | ||
![]() |
580fc08e0e | ||
![]() |
9cbc95a6db | ||
![]() |
fa749f3b45 | ||
![]() |
a5d18ef24c | ||
![]() |
f09f2df2e3 | ||
![]() |
86763bd3aa | ||
![]() |
22f21a270b | ||
![]() |
5944c3ca28 | ||
![]() |
c67b8ec91d | ||
![]() |
281aff1725 | ||
![]() |
158e37d3ae | ||
![]() |
2583e5113a | ||
![]() |
42cc7740a5 | ||
![]() |
bee29e8b7e | ||
![]() |
89a5de0105 | ||
![]() |
800760054e | ||
![]() |
8c1aa6b39b | ||
![]() |
7204ae2e19 | ||
![]() |
83d25e00fe | ||
![]() |
a9adc15ce9 | ||
![]() |
2ca5dbf071 | ||
![]() |
dcb9882978 |
47
.github/workflows/arm-registry.yml
vendored
Normal file
47
.github/workflows/arm-registry.yml
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
name: Create and publish a Docker image with ARM
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
env:
|
||||
CI: false
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
64
.github/workflows/docker-publish.yml
vendored
Normal file
64
.github/workflows/docker-publish.yml
vendored
Normal file
@ -0,0 +1,64 @@
|
||||
name: Docker
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
# Publish semver tags as releases.
|
||||
tags: [ 'v*.*.*' ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
REGISTRY: ghcr.io
|
||||
# github.repository as <account>/<repo>
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
PLATFORMS: linux/amd64,linux/arm64,linux/arm/v7,linux/386
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Extract metadata (tags, labels) for Docker
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: ${{ env.PLATFORMS }}
|
168
.goreleaser.yml
168
.goreleaser.yml
@ -4,80 +4,99 @@ before:
|
||||
hooks:
|
||||
- go mod download
|
||||
builds:
|
||||
- main: ./main.go
|
||||
id: speedtest-backend
|
||||
binary: speedtest-backend
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -w -s
|
||||
goos:
|
||||
- windows
|
||||
- linux
|
||||
- darwin
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- mips
|
||||
- mipsle
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
gomips:
|
||||
- hardfloat
|
||||
- softfloat
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
hooks:
|
||||
post: upx -9 "{{ .Path }}"
|
||||
- main: ./main.go
|
||||
id: speedtest-backend-freebsd
|
||||
binary: speedtest-backend
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -w -s
|
||||
goos:
|
||||
- freebsd
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- mips
|
||||
- mipsle
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
gomips:
|
||||
- hardfloat
|
||||
- softfloat
|
||||
- main: ./main.go
|
||||
id: speedtest-backend-noupx
|
||||
binary: speedtest-backend
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -w -s
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- mips64
|
||||
- mips64le
|
||||
gomips:
|
||||
- hardfloat
|
||||
- softfloat
|
||||
- main: ./main.go
|
||||
id: speedtest-backend
|
||||
binary: speedtest-backend
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -w -s
|
||||
goos:
|
||||
- windows
|
||||
- linux
|
||||
- darwin
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
hooks:
|
||||
post: upx -9 "{{ .Path }}"
|
||||
- main: ./main.go
|
||||
id: speedtest-backend-freebsd
|
||||
binary: speedtest-backend
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -w -s
|
||||
goos:
|
||||
- freebsd
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- mips
|
||||
- mipsle
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
gomips:
|
||||
- hardfloat
|
||||
- softfloat
|
||||
- main: ./main.go
|
||||
id: speedtest-backend-noupx-linux
|
||||
binary: speedtest-backend
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -w -s
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- mips
|
||||
- mipsle
|
||||
- mips64
|
||||
- mips64le
|
||||
gomips:
|
||||
- hardfloat
|
||||
- softfloat
|
||||
- main: ./main.go
|
||||
id: speedtest-backend-noupx-windows-arm64
|
||||
binary: speedtest-backend
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -w -s
|
||||
goos:
|
||||
- windows
|
||||
goarch:
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
archives:
|
||||
- format_overrides:
|
||||
- goos: windows
|
||||
@ -85,7 +104,6 @@ archives:
|
||||
files:
|
||||
- README.md
|
||||
- LICENSE
|
||||
- assets/*
|
||||
- settings.toml
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
|
26
Dockerfile
26
Dockerfile
@ -1,20 +1,18 @@
|
||||
FROM golang:alpine AS build_base
|
||||
#ENV GOARCH arm64
|
||||
#ENV GOARCH amd64
|
||||
RUN apk add --no-cache git gcc ca-certificates libc-dev \
|
||||
&& mkdir -p /go/src/github.com/librespeed/ \
|
||||
&& cd /go/src/github.com/librespeed/ \
|
||||
&& git clone https://github.com/librespeed/speedtest-go.git
|
||||
WORKDIR /go/src/github.com/librespeed/speedtest-go
|
||||
RUN go get ./ && go build -ldflags "-w -s" -trimpath -o speedtest main.go
|
||||
FROM golang:1.18-alpine AS build_base
|
||||
RUN apk add --no-cache git gcc ca-certificates libc-dev
|
||||
WORKDIR /build
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY ./ ./
|
||||
RUN go build -ldflags "-w -s" -trimpath -o speedtest .
|
||||
|
||||
FROM alpine:3.9
|
||||
RUN apk add ca-certificates
|
||||
FROM alpine:3.16
|
||||
RUN apk add --no-cache ca-certificates
|
||||
WORKDIR /app
|
||||
COPY --from=build_base /go/src/github.com/librespeed/speedtest-go/speedtest .
|
||||
COPY --from=build_base /go/src/github.com/librespeed/speedtest-go/assets ./assets
|
||||
COPY --from=build_base /go/src/github.com/librespeed/speedtest-go/settings.toml .
|
||||
COPY --from=build_base /build/speedtest ./
|
||||
COPY settings.toml ./
|
||||
|
||||
USER nobody
|
||||
EXPOSE 8989
|
||||
|
||||
CMD ["./speedtest"]
|
||||
|
39
README.md
39
README.md
@ -34,16 +34,29 @@ Works with mobile versions too.
|
||||
|
||||
## Installation
|
||||
|
||||
You need Go 1.13+ to compile the binary. If you have an older version of Go and don't want to install the tarball
|
||||
### Install using prebuilt binaries
|
||||
|
||||
1. Download the appropriate binary file from the [releases](https://github.com/librespeed/speedtest-go/releases/) page.
|
||||
2. Unzip the archive.
|
||||
3. Make changes to the configuration.
|
||||
4. Run the binary.
|
||||
5. Optional: Setup a systemd service file.
|
||||
|
||||
### Use Ansible for automatic installation
|
||||
|
||||
You can use an Ansible role for installing speedtest-go easily. You can find the role on the [Ansible galaxy](https://galaxy.ansible.com/flymia/ansible_speedtest_go). There is a [separate repository](https://github.com/flymia/ansible-speedtest_go) for documentation about the Ansible role.
|
||||
### Compile from source
|
||||
|
||||
You need Go 1.16+ to compile the binary. If you have an older version of Go and don't want to install the tarball
|
||||
manually, you can install newer version of Go into your `GOPATH`:
|
||||
|
||||
0. Install Go 1.14
|
||||
0. Install Go 1.17
|
||||
|
||||
```
|
||||
$ go get golang.org/dl/go1.14.2
|
||||
# Assuming your GOPATH is default (~/go), Go 1.14.2 will be installed in ~/go/bin
|
||||
$ ~/go/bin/go1.14.2 version
|
||||
go version go1.14.2 linux/amd64
|
||||
$ go get golang.org/dl/go1.17.1
|
||||
# Assuming your GOPATH is default (~/go), Go 1.17.1 will be installed in ~/go/bin
|
||||
$ ~/go/bin/go1.17.1 version
|
||||
go version go1.17.1 linux/amd64
|
||||
```
|
||||
|
||||
1. Clone this repository:
|
||||
@ -98,6 +111,7 @@ manually, you can install newer version of Go into your `GOPATH`:
|
||||
ipinfo_api_key=""
|
||||
|
||||
# assets directory path, defaults to `assets` in the same directory
|
||||
# if the path cannot be found, embedded default assets will be used
|
||||
assets_path="./assets"
|
||||
|
||||
# password for logging into statistics page, change this to enable stats page
|
||||
@ -105,7 +119,8 @@ manually, you can install newer version of Go into your `GOPATH`:
|
||||
# redact IP addresses
|
||||
redact_ip_addresses=false
|
||||
|
||||
# database type for statistics data, currently supports: bolt, mysql, postgresql
|
||||
# database type for statistics data, currently supports: none, memory, bolt, mysql, postgresql
|
||||
# if none is specified, no telemetry/stats will be recorded, and no result PNG will be generated
|
||||
database_type="postgresql"
|
||||
database_hostname="localhost"
|
||||
database_name="speedtest"
|
||||
@ -114,6 +129,14 @@ manually, you can install newer version of Go into your `GOPATH`:
|
||||
|
||||
# if you use `bolt` as database, set database_file to database file location
|
||||
database_file="speedtest.db"
|
||||
|
||||
# TLS and HTTP/2 settings. TLS is required for HTTP/2
|
||||
enable_tls=false
|
||||
enable_http2=false
|
||||
|
||||
# if you use HTTP/2 or TLS, you need to prepare certificates and private keys
|
||||
# tls_cert_file="cert.pem"
|
||||
# tls_key_file="privkey.pem"
|
||||
```
|
||||
|
||||
## Differences between Go and PHP implementation and caveats
|
||||
@ -142,4 +165,4 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/lgpl>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/lgpl>.
|
@ -1,365 +0,0 @@
|
||||
<!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>
|
@ -8,6 +8,7 @@ import (
|
||||
type Config struct {
|
||||
BindAddress string `mapstructure:"bind_address"`
|
||||
Port string `mapstructure:"listen_port"`
|
||||
BaseURL string `mapstructure:"url_base"`
|
||||
ProxyProtocolPort string `mapstructure:"proxyprotocol_port"`
|
||||
ServerLat float64 `mapstructure:"server_lat"`
|
||||
ServerLng float64 `mapstructure:"server_lng"`
|
||||
@ -25,26 +26,33 @@ type Config struct {
|
||||
DatabasePassword string `mapstructure:"database_password"`
|
||||
|
||||
DatabaseFile string `mapstructure:"database_file"`
|
||||
|
||||
EnableHTTP2 bool `mapstructure:"enable_http2"`
|
||||
EnableTLS bool `mapstructure:"enable_tls"`
|
||||
TLSCertFile string `mapstructure:"tls_cert_file"`
|
||||
TLSKeyFile string `mapstructure:"tls_key_file"`
|
||||
}
|
||||
|
||||
var (
|
||||
configFile string = ""
|
||||
configFile string
|
||||
loadedConfig *Config = nil
|
||||
)
|
||||
|
||||
func init() {
|
||||
viper.SetDefault("listen_port", "8989")
|
||||
viper.SetDefault("url_base", "")
|
||||
viper.SetDefault("proxyprotocol_port", "0")
|
||||
viper.SetDefault("download_chunks", 4)
|
||||
viper.SetDefault("distance_unit", "K")
|
||||
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")
|
||||
viper.SetDefault("database_username", "postgres")
|
||||
viper.SetDefault("enable_tls", false)
|
||||
viper.SetDefault("enable_http2", false)
|
||||
|
||||
viper.SetConfigName("settings")
|
||||
viper.AddConfigPath(".")
|
||||
@ -55,14 +63,9 @@ func Load(configPath string) Config {
|
||||
|
||||
configFile = configPath
|
||||
viper.SetConfigFile(configPath)
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||
log.Warnf("No config file found in search paths, using default values")
|
||||
} else {
|
||||
log.Fatalf("Error reading config: %s", err)
|
||||
}
|
||||
}
|
||||
viper.SetEnvPrefix("speedtest")
|
||||
viper.AutomaticEnv()
|
||||
viper.ReadInConfig()
|
||||
|
||||
if err := viper.Unmarshal(&conf); err != nil {
|
||||
log.Fatalf("Error parsing config: %s", err)
|
||||
|
@ -5,12 +5,10 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"go.etcd.io/bbolt"
|
||||
|
||||
"github.com/librespeed/speedtest/database/schema"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -3,9 +3,13 @@ package database
|
||||
import (
|
||||
"github.com/librespeed/speedtest/config"
|
||||
"github.com/librespeed/speedtest/database/bolt"
|
||||
"github.com/librespeed/speedtest/database/memory"
|
||||
"github.com/librespeed/speedtest/database/mysql"
|
||||
"github.com/librespeed/speedtest/database/none"
|
||||
"github.com/librespeed/speedtest/database/postgresql"
|
||||
"github.com/librespeed/speedtest/database/schema"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -26,5 +30,11 @@ func SetDBInfo(conf *config.Config) {
|
||||
DB = mysql.Open(conf.DatabaseHostname, conf.DatabaseUsername, conf.DatabasePassword, conf.DatabaseName)
|
||||
case "bolt":
|
||||
DB = bolt.Open(conf.DatabaseFile)
|
||||
case "memory":
|
||||
DB = memory.Open("")
|
||||
case "none":
|
||||
DB = none.Open("")
|
||||
default:
|
||||
log.Fatalf("Unsupported database type: %s", conf.DatabaseType)
|
||||
}
|
||||
}
|
||||
|
51
database/memory/memory.go
Normal file
51
database/memory/memory.go
Normal file
@ -0,0 +1,51 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/librespeed/speedtest/database/schema"
|
||||
)
|
||||
|
||||
const (
|
||||
// just enough records to return for FetchLast100
|
||||
maxRecords = 100
|
||||
)
|
||||
|
||||
type Memory struct {
|
||||
lock sync.RWMutex
|
||||
records []schema.TelemetryData
|
||||
}
|
||||
|
||||
func Open(_ string) *Memory {
|
||||
return &Memory{}
|
||||
}
|
||||
|
||||
func (mem *Memory) Insert(data *schema.TelemetryData) error {
|
||||
mem.lock.Lock()
|
||||
defer mem.lock.Unlock()
|
||||
data.Timestamp = time.Now()
|
||||
mem.records = append(mem.records, *data)
|
||||
if len(mem.records) > maxRecords {
|
||||
mem.records = mem.records[len(mem.records)-maxRecords:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mem *Memory) FetchByUUID(uuid string) (*schema.TelemetryData, error) {
|
||||
mem.lock.RLock()
|
||||
defer mem.lock.RUnlock()
|
||||
for _, record := range mem.records {
|
||||
if record.UUID == uuid {
|
||||
return &record, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("record not found")
|
||||
}
|
||||
|
||||
func (mem *Memory) FetchLast100() ([]schema.TelemetryData, error) {
|
||||
mem.lock.RLock()
|
||||
defer mem.lock.RUnlock()
|
||||
return mem.records, nil
|
||||
}
|
23
database/none/none.go
Normal file
23
database/none/none.go
Normal file
@ -0,0 +1,23 @@
|
||||
package none
|
||||
|
||||
import (
|
||||
"github.com/librespeed/speedtest/database/schema"
|
||||
)
|
||||
|
||||
type None struct{}
|
||||
|
||||
func Open(_ string) *None {
|
||||
return &None{}
|
||||
}
|
||||
|
||||
func (n *None) Insert(_ *schema.TelemetryData) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *None) FetchByUUID(_ string) (*schema.TelemetryData, error) {
|
||||
return &schema.TelemetryData{}, nil
|
||||
}
|
||||
|
||||
func (n *None) FetchLast100() ([]schema.TelemetryData, error) {
|
||||
return []schema.TelemetryData{}, nil
|
||||
}
|
37
go.mod
37
go.mod
@ -1,32 +1,25 @@
|
||||
module github.com/librespeed/speedtest
|
||||
|
||||
go 1.13
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.1
|
||||
github.com/go-chi/cors v1.1.1
|
||||
github.com/breml/rootcerts v0.2.1
|
||||
github.com/coreos/go-systemd/v22 v22.4.0
|
||||
github.com/go-chi/chi/v5 v5.0.7
|
||||
github.com/go-chi/cors v1.2.0
|
||||
github.com/go-chi/render v1.0.1
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
|
||||
github.com/lib/pq v1.10.0
|
||||
github.com/magiconair/properties v1.8.4 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/lib/pq v1.10.4
|
||||
github.com/oklog/ulid/v2 v2.0.2
|
||||
github.com/pelletier/go-toml v1.8.1 // indirect
|
||||
github.com/pires/go-proxyproto v0.5.0
|
||||
github.com/pires/go-proxyproto v0.6.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/spf13/afero v1.5.1 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.7.1
|
||||
github.com/spf13/afero v1.8.0 // indirect
|
||||
github.com/spf13/viper v1.10.1
|
||||
github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26
|
||||
go.etcd.io/bbolt v1.3.5
|
||||
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
|
||||
golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e // indirect
|
||||
golang.org/x/text v0.3.5 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
go.etcd.io/bbolt v1.3.6
|
||||
golang.org/x/crypto v0.9.0
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410
|
||||
)
|
||||
|
6
main.go
6
main.go
@ -2,13 +2,15 @@ package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
_ "time/tzdata"
|
||||
|
||||
"github.com/librespeed/speedtest/config"
|
||||
"github.com/librespeed/speedtest/database"
|
||||
"github.com/librespeed/speedtest/results"
|
||||
"github.com/librespeed/speedtest/web"
|
||||
|
||||
_ "github.com/breml/rootcerts"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -4,8 +4,12 @@ import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/librespeed/speedtest/config"
|
||||
"github.com/librespeed/speedtest/database"
|
||||
"github.com/librespeed/speedtest/database/schema"
|
||||
@ -17,6 +21,37 @@ type StatsData struct {
|
||||
Data []schema.TelemetryData
|
||||
}
|
||||
|
||||
var (
|
||||
store *sessions.CookieStore
|
||||
conf *config.Config
|
||||
checkPassword func(password string) bool
|
||||
)
|
||||
|
||||
func statsInitialize(c *config.Config) {
|
||||
key := []byte(securecookie.GenerateRandomKey(32))
|
||||
store = sessions.NewCookieStore(key)
|
||||
store.Options = &sessions.Options{
|
||||
Path: c.BaseURL + "/stats",
|
||||
MaxAge: 3600 * 1, // 1 hour
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
}
|
||||
conf = c
|
||||
|
||||
// Check if StatsPassword is a valid bcrypt hash
|
||||
if _, err := bcrypt.Cost([]byte(c.StatsPassword)); err == nil {
|
||||
log.Println("statistics_password is valid bcrypt hash")
|
||||
checkPassword = func(password string) bool {
|
||||
return nil == bcrypt.CompareHashAndPassword([]byte(c.StatsPassword), []byte(password))
|
||||
}
|
||||
|
||||
} else {
|
||||
checkPassword = func(password string) bool {
|
||||
return password == c.StatsPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Stats(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
t, err := template.New("template").Parse(htmlTemplate)
|
||||
@ -26,7 +61,11 @@ func Stats(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
conf := config.LoadedConfig()
|
||||
if conf.DatabaseType == "none" {
|
||||
render.PlainText(w, r, "Statistics are disabled")
|
||||
return
|
||||
}
|
||||
|
||||
var data StatsData
|
||||
|
||||
if conf.StatsPassword == "PASSWORD" {
|
||||
@ -35,16 +74,15 @@ func Stats(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if !data.NoPassword {
|
||||
op := r.FormValue("op")
|
||||
c, _ := r.Cookie("logged")
|
||||
session, _ := store.Get(r, "logged")
|
||||
auth, ok := session.Values["authenticated"].(bool)
|
||||
|
||||
if c != nil && c.Value == "true" {
|
||||
if auth && ok {
|
||||
if op == "logout" {
|
||||
cookie := &http.Cookie{
|
||||
Name: "logged",
|
||||
Value: "false",
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
http.Redirect(w, r, "/stats", http.StatusTemporaryRedirect)
|
||||
session.Values["authenticated"] = false
|
||||
session.Options.MaxAge = -1
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, conf.BaseURL+"/stats", http.StatusTemporaryRedirect)
|
||||
} else {
|
||||
data.LoggedIn = true
|
||||
|
||||
@ -71,14 +109,12 @@ func Stats(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
} else {
|
||||
if op == "login" {
|
||||
session, _ := store.Get(r, "logged")
|
||||
password := r.FormValue("password")
|
||||
if password == conf.StatsPassword {
|
||||
cookie := &http.Cookie{
|
||||
Name: "logged",
|
||||
Value: "true",
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
http.Redirect(w, r, "/stats", http.StatusTemporaryRedirect)
|
||||
if checkPassword(password) {
|
||||
session.Values["authenticated"] = true
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, conf.BaseURL+"/stats", http.StatusTemporaryRedirect)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
}
|
||||
|
@ -1,20 +1,20 @@
|
||||
package results
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
"github.com/librespeed/speedtest/config"
|
||||
"github.com/librespeed/speedtest/database"
|
||||
"github.com/librespeed/speedtest/database/schema"
|
||||
@ -37,6 +37,12 @@ const (
|
||||
labelUpload = "Upload"
|
||||
)
|
||||
|
||||
//go:embed fonts/NotoSansDisplay-Medium.ttf
|
||||
var fontMediumBytes []byte
|
||||
|
||||
//go:embed fonts/NotoSansDisplay-Light.ttf
|
||||
var fontLightBytes []byte
|
||||
|
||||
var (
|
||||
ipv4Regex = regexp.MustCompile(`(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)`)
|
||||
ipv6Regex = regexp.MustCompile(`(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))`)
|
||||
@ -81,27 +87,20 @@ type IPInfoResponse struct {
|
||||
}
|
||||
|
||||
func Initialize(c *config.Config) {
|
||||
statsInitialize(c)
|
||||
// changed to use Noto Sans instead of OpenSans, due to issue:
|
||||
// https://github.com/golang/freetype/issues/8
|
||||
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)
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing NotoSansDisplay-Light font: %s", err)
|
||||
}
|
||||
fontLight = f
|
||||
fLight, err := freetype.ParseFont(fontLightBytes)
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing NotoSansDisplay-Light font: %s", err)
|
||||
}
|
||||
fontLight = fLight
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing NotoSansDisplay-Medium font: %s", err)
|
||||
}
|
||||
fontBold = f
|
||||
fMedium, err := freetype.ParseFont(fontMediumBytes)
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing NotoSansDisplay-Medium font: %s", err)
|
||||
}
|
||||
fontBold = fMedium
|
||||
|
||||
pingJitterLabelFace = truetype.NewFace(fontBold, &truetype.Options{
|
||||
Size: 12,
|
||||
@ -147,6 +146,12 @@ func Initialize(c *config.Config) {
|
||||
}
|
||||
|
||||
func Record(w http.ResponseWriter, r *http.Request) {
|
||||
conf := config.LoadedConfig()
|
||||
if conf.DatabaseType == "none" {
|
||||
render.PlainText(w, r, "Telemetry is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
ipAddr, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
userAgent := r.UserAgent()
|
||||
language := r.Header.Get("Accept-Language")
|
||||
@ -204,6 +209,12 @@ func Record(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func DrawPNG(w http.ResponseWriter, r *http.Request) {
|
||||
conf := config.LoadedConfig()
|
||||
|
||||
if conf.DatabaseType == "none" {
|
||||
return
|
||||
}
|
||||
|
||||
uuid := r.FormValue("id")
|
||||
record, err := database.DB.FetchByUUID(uuid)
|
||||
if err != nil {
|
||||
|
14
rpm/el7/README.md
Normal file
14
rpm/el7/README.md
Normal file
@ -0,0 +1,14 @@
|
||||
# librespeedgo-rpm
|
||||
|
||||
Librespeedtest Go version package (tested for el7)
|
||||
upstream: https://github.com/librespeed/speedtest-go
|
||||
|
||||
custom rpmmacro vars:
|
||||
* hk_version - define version
|
||||
* hk_build - define build
|
||||
* godir - change default GOPATH
|
||||
|
||||
example:
|
||||
```
|
||||
rpmbuild -D 'hk_build 3' -D 'hk_version 1.1.3' -D 'godir %{_builddir}/%{name}/.go' -bb SPECS/librespeedgo.spec
|
||||
```
|
6
rpm/el7/SOURCES/librespeedgo.firewalld
Normal file
6
rpm/el7/SOURCES/librespeedgo.firewalld
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<service>
|
||||
<short>librespeedgo</short>
|
||||
<description>Libres speedtest service GO-version</description>
|
||||
<port protocol="tcp" port="8989"/>
|
||||
</service>
|
29
rpm/el7/SOURCES/librespeedgo.mainconfig
Normal file
29
rpm/el7/SOURCES/librespeedgo.mainconfig
Normal file
@ -0,0 +1,29 @@
|
||||
# bind address, use empty string to bind to all interfaces
|
||||
bind_address=""
|
||||
# backend listen port
|
||||
listen_port=8989
|
||||
# proxy protocol port, use 0 to disable
|
||||
proxyprotocol_port=0
|
||||
# Server location
|
||||
server_lat=0
|
||||
server_lng=0
|
||||
# ipinfo.io API key, if applicable
|
||||
ipinfo_api_key=""
|
||||
|
||||
# assets directory path, defaults to `assets` in the same directory
|
||||
assets_path="/usr/share/librespeedgo/assets"
|
||||
|
||||
# password for logging into statistics page
|
||||
statistics_password="PASSWORD"
|
||||
# redact IP addresses
|
||||
redact_ip_addresses=false
|
||||
|
||||
# database type for statistics data, currently supports: bolt, mysql, postgresql
|
||||
database_type="bolt"
|
||||
database_hostname=""
|
||||
database_name=""
|
||||
database_username=""
|
||||
database_password=""
|
||||
|
||||
# if you use `bolt` as database, set database_file to database file location
|
||||
database_file="/var/lib/librespeedgo/speedtest.db"
|
29
rpm/el7/SOURCES/librespeedgo.service
Normal file
29
rpm/el7/SOURCES/librespeedgo.service
Normal file
@ -0,0 +1,29 @@
|
||||
[Unit]
|
||||
Description=Librespeed speed test
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=librespeedgo
|
||||
Group=librespeedgo
|
||||
WorkingDirectory=/usr/share/librespeedgo/
|
||||
ExecStart=/usr/bin/librespeedgo -c /etc/librespeedgo/settings.toml
|
||||
|
||||
DevicePolicy=closed
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
PrivateUsers=yes
|
||||
ProtectControlGroups=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
ReadWritePaths=/var/lib/librespeedgo
|
||||
ReadWritePaths=/etc/librespeedgo/settings.toml
|
||||
PrivateDevices=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
MemoryDenyWriteExecute=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
101
rpm/el7/SPECS/librespeedgo.spec
Normal file
101
rpm/el7/SPECS/librespeedgo.spec
Normal file
@ -0,0 +1,101 @@
|
||||
%global appname librespeedgo
|
||||
%global debug_package %{nil}
|
||||
%global __os_install_post %(echo '%{__os_install_post}' | sed -e 's!/usr/lib[^[:space:]]*/brp-.*[[:space:]].*$!!g')
|
||||
|
||||
Name: %{appname}
|
||||
Version: %{hk_version}
|
||||
Release: %{hk_build}%{?dist}
|
||||
Summary: LibreSpeed go-backend server
|
||||
|
||||
Group: Applications/System
|
||||
License: LGPL
|
||||
URL: https://github.com/librespeed/speedtest-go
|
||||
Source0: %{name}.tar.gz
|
||||
Source1: %{name}.mainconfig
|
||||
Source2: %{name}.service
|
||||
Source3: %{name}.firewalld
|
||||
|
||||
AutoReq: no
|
||||
AutoProv: no
|
||||
BuildArch: x86_64
|
||||
BuildRequires: golang >= 1.13
|
||||
|
||||
%description
|
||||
Very lightweight speed test implemented in Javascript, using XMLHttpRequest and Web Workers.
|
||||
|
||||
%prep
|
||||
curl -sL 'https://github.com/librespeed/speedtest-go/archive/refs/tags/v%{version}.tar.gz' -o %{_sourcedir}/%{name}.tar.gz
|
||||
if [[ -d %{_builddir}/%{name} ]];then
|
||||
chmod 777 -R %{_builddir}/%{name}
|
||||
rm -rf %{_builddir}/%{name}
|
||||
fi
|
||||
mkdir %{_builddir}/%{name}
|
||||
tar xf %{_sourcedir}/%{name}.tar.gz -C %{_builddir}/%{name} --strip-components 1
|
||||
cd %{_builddir}/%{name}
|
||||
cat << EOF >> %{name}.runtime
|
||||
d /var/lib/librespeedgo 0750 librespeedgo librespeedgo
|
||||
f /etc/librespeedgo/settings.toml 0640 root librespeedgo
|
||||
f /var/lib/librespeedgo/speedtest.db 0640 librespeedgo librespeedgo
|
||||
EOF
|
||||
cp -a %{SOURCE1} %{SOURCE2} %{SOURCE3} ./
|
||||
pushd %{_builddir}/%{name}/assets
|
||||
sed -i "s/LibreSpeed Example/LibreSpeed/" *.html
|
||||
popd
|
||||
|
||||
%build
|
||||
pushd %{_builddir}/%{name}
|
||||
%if 0%{?godir:1}
|
||||
GOPATH=%{godir} go build -ldflags "-w -s" -trimpath -o %{name} main.go
|
||||
%else
|
||||
go build -ldflags "-w -s" -trimpath -o %{name} main.go
|
||||
%endif
|
||||
popd
|
||||
|
||||
%install
|
||||
pushd %{_builddir}/%{name}
|
||||
install -D %{name} %{buildroot}%{_bindir}/%{name}
|
||||
install -Dm644 %{name}.runtime %{buildroot}%{_sysconfdir}/tmpfiles.d/%{name}.conf
|
||||
install -Dm640 %{name}.mainconfig %{buildroot}%{_sysconfdir}/%{name}/settings.toml
|
||||
install -Dm644 %{name}.service %{buildroot}%{_prefix}/lib/systemd/system/%{name}.service
|
||||
install -Dm644 %{name}.firewalld %{buildroot}%{_prefix}/lib/firewalld/services/%{name}.xml
|
||||
install -dm750 %{buildroot}/var/lib/%{name}
|
||||
|
||||
install -d %{buildroot}/%{_datadir}/%{name}
|
||||
cp -r assets %{buildroot}/%{_datadir}/%{name}
|
||||
install -m644 database/mysql/telemetry_mysql.sql %{buildroot}/%{_datadir}/%{name}
|
||||
install -m644 database/postgresql/telemetry_postgresql.sql %{buildroot}/%{_datadir}/%{name}
|
||||
popd
|
||||
|
||||
%files
|
||||
%config(noreplace) %{_sysconfdir}/%{name}/settings.toml
|
||||
%config(noreplace) %{_prefix}/lib/firewalld/services/%{name}.xml
|
||||
%config %{_sysconfdir}/tmpfiles.d/%{name}.conf
|
||||
%config %{_prefix}/lib/systemd/system/%{name}.service
|
||||
%{_bindir}/%{name}
|
||||
%{_datadir}/%{name}
|
||||
/var/lib/%{name}
|
||||
|
||||
%post
|
||||
if [ $1 == 1 ];then
|
||||
if ! getent passwd %{name} > /dev/null; then
|
||||
useradd -r -s /bin/false -m -d /var/lib/%{name} %{name}
|
||||
fi
|
||||
touch /var/lib/%{name}/speedtest.db
|
||||
chown -R %{name}:%{name} /var/lib/%{name}
|
||||
systemctl daemon-reload
|
||||
elif [ $1 == 2 ];then
|
||||
chown -R %{name}:%{name} /var/lib/%{name}
|
||||
systemctl daemon-reload
|
||||
if [ $(systemctl is-active --quiet %{name}.service) ];then
|
||||
systemctl restart %{name}.service
|
||||
fi
|
||||
fi
|
||||
|
||||
%preun
|
||||
if [ $1 == 0 ];then
|
||||
if [ $(systemctl is-active --quiet %{name}.service) ];then
|
||||
systemctl stop %{name}.service
|
||||
fi
|
||||
fi
|
||||
|
||||
%changelog
|
@ -2,24 +2,27 @@
|
||||
bind_address=""
|
||||
# backend listen port
|
||||
listen_port=8989
|
||||
# change the base URL
|
||||
# url_base="/librespeed"
|
||||
# proxy protocol port, use 0 to disable
|
||||
proxyprotocol_port=0
|
||||
# Server location
|
||||
server_lat=0
|
||||
server_lng=0
|
||||
server_lat=1
|
||||
server_lng=1
|
||||
# ipinfo.io API key, if applicable
|
||||
ipinfo_api_key=""
|
||||
|
||||
# assets directory path, defaults to `assets` in the same directory
|
||||
assets_path="./assets"
|
||||
assets_path=""
|
||||
|
||||
# password for logging into statistics page
|
||||
statistics_password="PASSWORD"
|
||||
# redact IP addresses
|
||||
redact_ip_addresses=false
|
||||
|
||||
# database type for statistics data, currently supports: bolt, mysql, postgresql
|
||||
database_type="bolt"
|
||||
# database type for statistics data, currently supports: none, memory, bolt, mysql, postgresql
|
||||
# if none is specified, no telemetry/stats will be recorded, and no result PNG will be generated
|
||||
database_type="memory"
|
||||
database_hostname=""
|
||||
database_name=""
|
||||
database_username=""
|
||||
@ -27,3 +30,11 @@ database_password=""
|
||||
|
||||
# if you use `bolt` as database, set database_file to database file location
|
||||
database_file="speedtest.db"
|
||||
|
||||
# TLS and HTTP/2 settings. TLS is required for HTTP/2
|
||||
enable_tls=false
|
||||
enable_http2=false
|
||||
|
||||
# if you use HTTP/2 or TLS, you need to prepare certificates and private keys
|
||||
# tls_cert_file="cert.pem"
|
||||
# tls_key_file="privkey.pem"
|
||||
|
28
systemd/README.md
Normal file
28
systemd/README.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Example systemd unit files
|
||||
|
||||
To use these, first review the speedtest.* unit files, and then:
|
||||
|
||||
cp ../speedtest /usr/local/bin/
|
||||
mkdir -p /usr/local/share/speedtest /usr/local/etc
|
||||
cp -aR ../web/assets /usr/local/share/speedtest/assets
|
||||
cp speedtest-settings.toml /usr/local/etc
|
||||
cp speedtest.* /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
|
||||
If you wish to use the bolt database type:
|
||||
|
||||
# Create static system user and group
|
||||
adduser --system --group --no-create-home --disabled-password speedtest
|
||||
mkdir -p /usr/local/var/speedtest
|
||||
touch /usr/local/var/speedtest/speedtest.db
|
||||
chown speedtest. /usr/local/var/speedtest/speedtest.db
|
||||
|
||||
To start (and enable at boot-up):
|
||||
|
||||
systemctl enable --now speedtest.socket
|
||||
|
||||
speedtest-go should now be listening for http request on port 80 on the local
|
||||
machine.
|
||||
|
||||
You will need to customise the html files e.g. edit
|
||||
`/usr/local/share/speedtest/assets/index.html` to suit your site.
|
31
systemd/speedtest-settings.toml
Normal file
31
systemd/speedtest-settings.toml
Normal file
@ -0,0 +1,31 @@
|
||||
# bind address, use empty string to bind to all interfaces, or when using socket activation
|
||||
#bind_address=""
|
||||
# backend listen port. Set this to "" when using socket activation
|
||||
listen_port=""
|
||||
# proxy protocol port, use 0 to disable
|
||||
proxyprotocol_port=0
|
||||
# Server location
|
||||
server_lat=50.82589
|
||||
server_lng=-0.141391
|
||||
# ipinfo.io API key, if applicable
|
||||
ipinfo_api_key=""
|
||||
|
||||
# assets directory path, defaults to `assets` in the same directory
|
||||
assets_path="/usr/local/share/speedtest/assets"
|
||||
|
||||
# password for logging into statistics page
|
||||
statistics_password="PASSWORD"
|
||||
# redact IP addresses
|
||||
redact_ip_addresses=false
|
||||
|
||||
# database type for statistics data, currently supports: none, memory, bolt, mysql, postgresql
|
||||
# if none is specified, no telemetry/stats will be recorded, and no result PNG will be generated
|
||||
#database_type="bolt"
|
||||
database_type="memory"
|
||||
database_hostname=""
|
||||
database_name=""
|
||||
database_username=""
|
||||
database_password=""
|
||||
|
||||
# if you use `bolt` as database, set database_file to database file location
|
||||
database_file="/usr/local/var/speedtest/speedtest.db"
|
135
systemd/speedtest.service
Normal file
135
systemd/speedtest.service
Normal file
@ -0,0 +1,135 @@
|
||||
# Systemd unit file for speedtest-go. The defaults below are suitable for
|
||||
# running all configurations in a medium-security environment. See comments
|
||||
# below for addtional caveats - particularly those labelled "IMPORTANT".
|
||||
|
||||
# You can edit this file, or alternatively you may prefer to use systemd's
|
||||
# "override" mechanisms, to avoid editing this file e.g. using:
|
||||
|
||||
# systemctl edit speedtest.service
|
||||
|
||||
[Unit]
|
||||
Description=Speedtest-go Server
|
||||
After=syslog.target network.target
|
||||
|
||||
# Default to using socket activation (see accompanying socket unit file to
|
||||
# configure the bind address etc.).
|
||||
Requires=speedtest.socket
|
||||
After=speedtest.socket
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# The paths to the installed binary and configuration file:
|
||||
|
||||
ExecStart=/usr/local/bin/speedtest -c /usr/local/etc/speedtest-settings.toml
|
||||
#WorkingDirectory=/usr/local/share/speedtest
|
||||
#Restart=always
|
||||
#RestartSec=5
|
||||
|
||||
# IMPORTANT!
|
||||
# If you use a database file (not server), then you will need to disable the
|
||||
# DynamicUser setting, and manually create the UNIX user and group specified
|
||||
# below, to ensure the file is accessible across multiple invocations of the
|
||||
# service.
|
||||
DynamicUser=true
|
||||
|
||||
# You may prefer to use a different user or group name on your system.
|
||||
User=speedtest
|
||||
Group=speedtest
|
||||
|
||||
|
||||
# The following options will work for all configurations, but are not the
|
||||
# most secure, so you are advised to customise them as described below:
|
||||
|
||||
# If NOT using socket activation, or if using socket activation AND
|
||||
# connecting to an external database server (MySQL, postgres) via TCP:
|
||||
RestrictAddressFamilies=AF_INET AF_INET6
|
||||
|
||||
# If connecting to an external database via unix domain sockets (MySQL
|
||||
# default to this mode of operation):
|
||||
RestrictAddressFamilies=AF_UNIX
|
||||
|
||||
# If using 'none', 'memory', or 'bolt' database types, and socket activation
|
||||
# then the process will not need to bind to any new sockets, so we can remove
|
||||
# the earlier AF_UNIX option again. In systemd versions before 249 this is
|
||||
# the only way to say "Restrict the use of all address families":
|
||||
RestrictAddressFamilies=AF_UNIX
|
||||
RestrictAddressFamilies=~AF_UNIX
|
||||
# ...in systemd version 249 and later, we can instead use the much clearer:
|
||||
#RestrictAddressFamilies=none
|
||||
|
||||
# The following options are available (in systemd v247) to restrict the
|
||||
# actions of the speedtest server for reasons of increased security.
|
||||
|
||||
# As a whole, the purpose of these are to provide an additional layer of
|
||||
# security by mitigating any unknown security vulnerabilities which may exist
|
||||
# in speedtest or in the libraries, tools and operating system components
|
||||
# which it relies upon.
|
||||
|
||||
# IMPORTANT!
|
||||
# The following line must be customised to your individual requirements.
|
||||
# e.g. if using the 'bolt' in-process database type:
|
||||
ReadWritePaths=/usr/local/var/speedtest
|
||||
|
||||
# Makes created files group-readable, but inaccessible by others
|
||||
UMask=027
|
||||
|
||||
# Many of the following options are desribed in the systemd.resource-control(5)
|
||||
# manual page.
|
||||
|
||||
# The following may be useful in your environment:
|
||||
#IPAddressDeny=
|
||||
#IPAddressAllow=
|
||||
#IPAccounting=true
|
||||
#IPIngressFilterPath=
|
||||
#SocketBindAllow=
|
||||
|
||||
# If your system doesn't support all of the features below (e.g. because of
|
||||
# the use of a version of systemd older than 247), you may need to comment-out
|
||||
# some of the following lines.
|
||||
|
||||
# n.b. It may be possible to further restrict speedtest, but this is a good
|
||||
# start, and will guard against many potential zero-day vulnerabilities.
|
||||
|
||||
# See the output of `systemd-analyze security speedtest.service` for further
|
||||
# opportunities. Patches welcome!
|
||||
|
||||
CapabilityBoundingSet=
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=true
|
||||
PrivateUsers=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
ProtectClock=true
|
||||
ProtectControlGroups=true
|
||||
ProtectKernelLogs=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectProc=invisible
|
||||
ProtectHostname=true
|
||||
RemoveIPC=true
|
||||
RestrictNamespaces=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictRealtime=true
|
||||
SystemCallArchitectures=native
|
||||
SystemCallFilter=@system-service
|
||||
|
||||
# Additionally, you may wish to use some of the systemd options documented in
|
||||
# systemd.resource-control(5) to limit the CPU, memory, file-system I/O and
|
||||
# network I/O that the speedtest server is permitted to consume according to
|
||||
# the individual requirements of your installation.
|
||||
|
||||
#CPUQuota=25%
|
||||
#MemoryMax=bytes
|
||||
#MemorySwapMax=bytes
|
||||
#TasksMax=N
|
||||
#IOReadBandwidthMax=device bytes
|
||||
#IOWriteBandwidthMax=device bytes
|
||||
#IOReadIOPSMax=device IOPS, IOWriteIOPSMax=device IOPS
|
||||
#IPAccounting=true
|
||||
#IPAddressAllow=
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
11
systemd/speedtest.socket
Normal file
11
systemd/speedtest.socket
Normal file
@ -0,0 +1,11 @@
|
||||
# Socket listener systemd unit file for speedtest-go. See the
|
||||
# systemd.socket(5) manual page for many more options.
|
||||
[Unit]
|
||||
Description=Speedtest Web Server (http port 80) Socket
|
||||
|
||||
[Socket]
|
||||
ListenStream=80
|
||||
Accept=no
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
107
assets/example-multipleServers-full.html → web/assets/example-multipleServers-full.html
Normal file → Executable file
107
assets/example-multipleServers-full.html → web/assets/example-multipleServers-full.html
Normal file → Executable file
@ -10,21 +10,21 @@ function I(i){return document.getElementById(i);}
|
||||
|
||||
//LIST OF TEST SERVERS. See documentation for details if needed
|
||||
var SPEEDTEST_SERVERS=[
|
||||
{ //this is my demo server, remove it
|
||||
name:"Speedtest Demo Server (Helsinki)", //user friendly name for the server
|
||||
server:"//fi.openspeed.org/", //URL to the server. // at the beginning will be replaced with http:// or https:// automatically
|
||||
dlURL:"garbage", //path to download test on this server (garbage.php or replacement)
|
||||
ulURL:"empty", //path to upload test on this server (empty.php or replacement)
|
||||
pingURL:"empty", //path to ping/jitter test on this server (empty.php or replacement)
|
||||
getIpURL:"getIP" //path to getIP on this server (getIP.php or replacement)
|
||||
{ //this server doesn't actually exist, remove it
|
||||
name:"Example Server 1", //user friendly name for the server
|
||||
server:"//test1.mydomain.com/", //URL to the server. // at the beginning will be replaced with http:// or https:// automatically
|
||||
dlURL:"backend/garbage.php", //path to download test on this server (garbage.php or replacement)
|
||||
ulURL:"backend/empty.php", //path to upload test on this server (empty.php or replacement)
|
||||
pingURL:"backend/empty.php", //path to ping/jitter test on this server (empty.php or replacement)
|
||||
getIpURL:"backend/getIP.php" //path to getIP on this server (getIP.php or replacement)
|
||||
},
|
||||
{ //this is my demo server, remove it
|
||||
name:"Old Speedtest Demo Server",
|
||||
server:"//mpotdemo.fdossena.com/",
|
||||
dlURL:"garbage",
|
||||
ulURL:"empty",
|
||||
pingURL:"empty",
|
||||
getIpURL:"getIP"
|
||||
{ //this server doesn't actually exist, remove it
|
||||
name:"Example Server 2", //user friendly name for the server
|
||||
server:"//test2.example.com/", //URL to the server. // at the beginning will be replaced with http:// or https:// automatically
|
||||
dlURL:"garbage.php", //path to download test on this server (garbage.php or replacement)
|
||||
ulURL:"empty.php", //path to upload test on this server (empty.php or replacement)
|
||||
pingURL:"empty.php", //path to ping/jitter test on this server (empty.php or replacement)
|
||||
getIpURL:"getIP.php" //path to getIP on this server (getIP.php or replacement)
|
||||
}
|
||||
//add other servers here, comma separated
|
||||
];
|
||||
@ -32,29 +32,48 @@ var SPEEDTEST_SERVERS=[
|
||||
//INITIALIZE SPEEDTEST
|
||||
var s=new Speedtest(); //create speedtest object
|
||||
s.setParameter("telemetry_level","basic"); //enable telemetry
|
||||
s.addTestPoints(SPEEDTEST_SERVERS); //add list of servers
|
||||
|
||||
//SERVER AUTO SELECTION
|
||||
function initServers(){
|
||||
s.selectServer(function(server){
|
||||
if(server!=null){ //at least 1 server is available
|
||||
I("loading").className="hidden"; //hide loading message
|
||||
//populate server list for manual selection
|
||||
for(var i=0;i<SPEEDTEST_SERVERS.length;i++){
|
||||
if(SPEEDTEST_SERVERS[i].pingT==-1) continue;
|
||||
var option=document.createElement("option");
|
||||
option.value=i;
|
||||
option.textContent=SPEEDTEST_SERVERS[i].name;
|
||||
if(SPEEDTEST_SERVERS[i]===server) option.selected=true;
|
||||
I("server").appendChild(option);
|
||||
}
|
||||
//show test UI
|
||||
I("testWrapper").className="visible";
|
||||
initUI();
|
||||
}else{ //no servers are available, the test cannot proceed
|
||||
I("message").innerHTML="No servers available";
|
||||
}
|
||||
});
|
||||
var noServersAvailable=function(){
|
||||
I("message").innerHTML="No servers available";
|
||||
}
|
||||
var runServerSelect=function(){
|
||||
s.selectServer(function(server){
|
||||
if(server!=null){ //at least 1 server is available
|
||||
I("loading").className="hidden"; //hide loading message
|
||||
//populate server list for manual selection
|
||||
for(var i=0;i<SPEEDTEST_SERVERS.length;i++){
|
||||
if(SPEEDTEST_SERVERS[i].pingT==-1) continue;
|
||||
var option=document.createElement("option");
|
||||
option.value=i;
|
||||
option.textContent=SPEEDTEST_SERVERS[i].name;
|
||||
if(SPEEDTEST_SERVERS[i]===server) option.selected=true;
|
||||
I("server").appendChild(option);
|
||||
}
|
||||
//show test UI
|
||||
I("testWrapper").className="visible";
|
||||
initUI();
|
||||
}else{ //no servers are available, the test cannot proceed
|
||||
noServersAvailable();
|
||||
}
|
||||
});
|
||||
}
|
||||
if(typeof SPEEDTEST_SERVERS === "string"){
|
||||
//need to fetch list of servers from specified URL
|
||||
s.loadServerList(SPEEDTEST_SERVERS,function(servers){
|
||||
if(servers==null){ //failed to load server list
|
||||
noServersAvailable();
|
||||
}else{ //server list loaded
|
||||
SPEEDTEST_SERVERS=servers;
|
||||
runServerSelect();
|
||||
}
|
||||
});
|
||||
}else{
|
||||
//hardcoded server list
|
||||
s.addTestPoints(SPEEDTEST_SERVERS);
|
||||
runServerSelect();
|
||||
}
|
||||
}
|
||||
|
||||
var meterBk=/Trident.*rv:(\d+\.\d+)/i.test(navigator.userAgent)?"#EAEAEA":"#80808040";
|
||||
@ -333,8 +352,15 @@ function initUI(){
|
||||
text-align:center;
|
||||
font-size:0.8em;
|
||||
color:#808080;
|
||||
display:block;
|
||||
padding: 0 3em;
|
||||
}
|
||||
div.closePrivacyPolicy {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
div.closePrivacyPolicy a.privacy {
|
||||
padding: 1em 3em;
|
||||
}
|
||||
@media all and (max-width:40em){
|
||||
body{
|
||||
font-size:0.8em;
|
||||
@ -375,7 +401,7 @@ function initUI(){
|
||||
<p id="message"><span class="loadCircle"></span>Selecting a server...</p>
|
||||
</div>
|
||||
<div id="testWrapper" class="hidden">
|
||||
<div id="startStopBtn" onclick="startStop()"></div>
|
||||
<div id="startStopBtn" onclick="startStop()"></div><br/>
|
||||
<a class="privacy" href="#" onclick="I('privacyPolicy').style.display=''">Privacy</a>
|
||||
<div id="serverArea">
|
||||
Server: <select id="server" onchange="s.setSelectedServer(SPEEDTEST_SERVERS[this.value])"></select>
|
||||
@ -398,13 +424,13 @@ function initUI(){
|
||||
<div class="testName">Download</div>
|
||||
<canvas id="dlMeter" class="meter"></canvas>
|
||||
<div id="dlText" class="meterText"></div>
|
||||
<div class="unit">Mbit/s</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">Mbit/s</div>
|
||||
<div class="unit">Mbps</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ipArea">
|
||||
@ -455,7 +481,10 @@ function initUI(){
|
||||
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 class="closePrivacyPolicy">
|
||||
<a class="privacy" href="#" onclick="I('privacyPolicy').style.display='none'">Close</a>
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
64
assets/example-multipleServers-pretty.html → web/assets/example-multipleServers-pretty.html
Normal file → Executable file
64
assets/example-multipleServers-pretty.html → web/assets/example-multipleServers-pretty.html
Normal file → Executable file
@ -10,21 +10,21 @@
|
||||
|
||||
//LIST OF TEST SERVERS. See documentation for details if needed
|
||||
var SPEEDTEST_SERVERS=[
|
||||
{ //this is my demo server, remove it
|
||||
name:"Speedtest Demo Server (Helsinki)", //user friendly name for the server
|
||||
server:"//fi.openspeed.org/", //URL to the server. // at the beginning will be replaced with http:// or https:// automatically
|
||||
dlURL:"garbage", //path to download test on this server (garbage.php or replacement)
|
||||
ulURL:"empty", //path to upload test on this server (empty.php or replacement)
|
||||
pingURL:"empty", //path to ping/jitter test on this server (empty.php or replacement)
|
||||
getIpURL:"getIP" //path to getIP on this server (getIP.php or replacement)
|
||||
{ //this server doesn't actually exist, remove it
|
||||
name:"Example Server 1", //user friendly name for the server
|
||||
server:"//test1.mydomain.com/", //URL to the server. // at the beginning will be replaced with http:// or https:// automatically
|
||||
dlURL:"backend/garbage.php", //path to download test on this server (garbage.php or replacement)
|
||||
ulURL:"backend/empty.php", //path to upload test on this server (empty.php or replacement)
|
||||
pingURL:"backend/empty.php", //path to ping/jitter test on this server (empty.php or replacement)
|
||||
getIpURL:"backend/getIP.php" //path to getIP on this server (getIP.php or replacement)
|
||||
},
|
||||
{ //this is my demo server, remove it
|
||||
name:"Old Speedtest Demo Server",
|
||||
server:"//mpotdemo.fdossena.com/",
|
||||
dlURL:"garbage",
|
||||
ulURL:"empty",
|
||||
pingURL:"empty",
|
||||
getIpURL:"getIP"
|
||||
{ //this server doesn't actually exist, remove it
|
||||
name:"Example Server 2", //user friendly name for the server
|
||||
server:"//test2.example.com/", //URL to the server. // at the beginning will be replaced with http:// or https:// automatically
|
||||
dlURL:"garbage.php", //path to download test on this server (garbage.php or replacement)
|
||||
ulURL:"empty.php", //path to upload test on this server (empty.php or replacement)
|
||||
pingURL:"empty.php", //path to ping/jitter test on this server (empty.php or replacement)
|
||||
getIpURL:"getIP.php" //path to getIP on this server (getIP.php or replacement)
|
||||
}
|
||||
//add other servers here, comma separated
|
||||
];
|
||||
@ -32,7 +32,7 @@ var SPEEDTEST_SERVERS=[
|
||||
|
||||
//INITIALIZE SPEEDTEST
|
||||
var s=new Speedtest(); //create speedtest object
|
||||
s.addTestPoints(SPEEDTEST_SERVERS); //add list of servers
|
||||
|
||||
s.onupdate=function(data){ //callback to update data in UI
|
||||
I("ip").textContent=data.clientIp;
|
||||
I("dlText").textContent=(data.testState==1&&data.dlStatus==0)?"...":data.dlStatus;
|
||||
@ -46,13 +46,33 @@ s.onend=function(aborted){ //callback for test ended/aborted
|
||||
initUI();
|
||||
}
|
||||
}
|
||||
function selectServer(){ //called when the page is fully loaded
|
||||
I("startStopBtn").style.display="none"; //hide start/stop button during server selection
|
||||
function selectServer(){ //called after loading server list
|
||||
s.selectServer(function(server){ //run server selection. When the server has been selected, display it in the UI
|
||||
I("startStopBtn").style.display=""; //show start/stop button again
|
||||
I("serverId").textContent=server.name; //show name of test server
|
||||
if(server==null){
|
||||
I("serverId").textContent="No servers available";
|
||||
}else{
|
||||
I("startStopBtn").style.display=""; //show start/stop button again
|
||||
I("serverId").textContent=server.name; //show name of test server
|
||||
}
|
||||
});
|
||||
}
|
||||
function loadServers(){ //called when the page is fully loaded
|
||||
I("startStopBtn").style.display="none"; //hide start/stop button during server selection
|
||||
if(typeof SPEEDTEST_SERVERS === "string"){
|
||||
//load servers from url
|
||||
s.loadServerList(SPEEDTEST_SERVERS,function(servers){
|
||||
//list loaded
|
||||
SPEEDTEST_SERVERS=servers;
|
||||
selectServer();
|
||||
});
|
||||
}else{
|
||||
//hardcoded list of servers, already loaded
|
||||
s.addTestPoints(SPEEDTEST_SERVERS);
|
||||
selectServer();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function startStop(){ //start/stop button pressed
|
||||
@ -192,12 +212,12 @@ function I(id){return document.getElementById(id);}
|
||||
<div class="testArea">
|
||||
<div class="testName">Download</div>
|
||||
<div id="dlText" class="meterText"></div>
|
||||
<div class="unit">Mbit/s</div>
|
||||
<div class="unit">Mbps</div>
|
||||
</div>
|
||||
<div class="testArea">
|
||||
<div class="testName">Upload</div>
|
||||
<div id="ulText" class="meterText"></div>
|
||||
<div class="unit">Mbit/s</div>
|
||||
<div class="unit">Mbps</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="testGroup">
|
||||
@ -219,7 +239,7 @@ function I(id){return document.getElementById(id);}
|
||||
<a href="https://github.com/librespeed/speedtest">Source code</a>
|
||||
<script type="text/javascript">
|
||||
initUI();
|
||||
selectServer();
|
||||
loadServers();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
0
assets/example-singleServer-basic.html → web/assets/example-singleServer-basic.html
Normal file → Executable file
0
assets/example-singleServer-basic.html → web/assets/example-singleServer-basic.html
Normal file → Executable file
0
assets/example-singleServer-chart.html → web/assets/example-singleServer-chart.html
Normal file → Executable file
0
assets/example-singleServer-chart.html → web/assets/example-singleServer-chart.html
Normal file → Executable file
4
assets/example-singleServer-customSettings.html → web/assets/example-singleServer-customSettings.html
Normal file → Executable file
4
assets/example-singleServer-customSettings.html → web/assets/example-singleServer-customSettings.html
Normal file → Executable file
@ -157,12 +157,12 @@ function I(id){return document.getElementById(id);}
|
||||
<div class="testArea">
|
||||
<div class="testName">Download</div>
|
||||
<div id="dlText" class="meterText"></div>
|
||||
<div class="unit">Mbit/s</div>
|
||||
<div class="unit">Mbps</div>
|
||||
</div>
|
||||
<div class="testArea">
|
||||
<div class="testName">Upload</div>
|
||||
<div id="ulText" class="meterText"></div>
|
||||
<div class="unit">Mbit/s</div>
|
||||
<div class="unit">Mbps</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
4
assets/example-singleServer-gauges.html → web/assets/example-singleServer-gauges.html
Normal file → Executable file
4
assets/example-singleServer-gauges.html → web/assets/example-singleServer-gauges.html
Normal file → Executable file
@ -242,13 +242,13 @@ function initUI(){
|
||||
<div class="testName">Download</div>
|
||||
<canvas id="dlMeter" class="meter"></canvas>
|
||||
<div id="dlText" class="meterText"></div>
|
||||
<div class="unit">Mbit/s</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">Mbit/s</div>
|
||||
<div class="unit">Mbps</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ipArea">
|
4
assets/example-singleServer-pretty.html → web/assets/example-singleServer-pretty.html
Normal file → Executable file
4
assets/example-singleServer-pretty.html → web/assets/example-singleServer-pretty.html
Normal file → Executable file
@ -160,12 +160,12 @@ function I(id){return document.getElementById(id);}
|
||||
<div class="testArea">
|
||||
<div class="testName">Download</div>
|
||||
<div id="dlText" class="meterText"></div>
|
||||
<div class="unit">Mbit/s</div>
|
||||
<div class="unit">Mbps</div>
|
||||
</div>
|
||||
<div class="testArea">
|
||||
<div class="testName">Upload</div>
|
||||
<div id="ulText" class="meterText"></div>
|
||||
<div class="unit">Mbit/s</div>
|
||||
<div class="unit">Mbps</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="testGroup">
|
4
assets/example-singleServer-progressBar.html → web/assets/example-singleServer-progressBar.html
Normal file → Executable file
4
assets/example-singleServer-progressBar.html → web/assets/example-singleServer-progressBar.html
Normal file → Executable file
@ -180,12 +180,12 @@ function I(id){return document.getElementById(id);}
|
||||
<div class="testArea">
|
||||
<div class="testName">Download</div>
|
||||
<div id="dlText" class="meterText"></div>
|
||||
<div class="unit">Mbit/s</div>
|
||||
<div class="unit">Mbps</div>
|
||||
</div>
|
||||
<div class="testArea">
|
||||
<div class="testName">Upload</div>
|
||||
<div id="ulText" class="meterText"></div>
|
||||
<div class="unit">Mbit/s</div>
|
||||
<div class="unit">Mbps</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="testGroup">
|
32
assets/index.html → web/assets/index.html
Normal file → Executable file
32
assets/index.html → web/assets/index.html
Normal file → Executable file
@ -3,23 +3,13 @@
|
||||
<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">
|
||||
<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",
|
||||
@ -268,8 +258,15 @@ function initUI(){
|
||||
text-align:center;
|
||||
font-size:0.8em;
|
||||
color:#808080;
|
||||
display:block;
|
||||
}
|
||||
padding: 0 3em;
|
||||
}
|
||||
div.closePrivacyPolicy {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
div.closePrivacyPolicy a.privacy {
|
||||
padding: 1em 3em;
|
||||
}
|
||||
@media all and (max-width:40em){
|
||||
body{
|
||||
font-size:0.8em;
|
||||
@ -301,13 +298,13 @@ function initUI(){
|
||||
<div class="testName">Download</div>
|
||||
<canvas id="dlMeter" class="meter"></canvas>
|
||||
<div id="dlText" class="meterText"></div>
|
||||
<div class="unit">Mbit/s</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">Mbit/s</div>
|
||||
<div class="unit">Mbps</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ipArea">
|
||||
@ -358,7 +355,10 @@ function initUI(){
|
||||
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 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>
|
30
assets/speedtest.js → web/assets/speedtest.js
Normal file → Executable file
30
assets/speedtest.js → web/assets/speedtest.js
Normal file → Executable file
@ -28,8 +28,8 @@
|
||||
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 Mbit/s
|
||||
- ulStatus: upload speed in Mbit/s
|
||||
- dlStatus: download speed in mbps
|
||||
- ulStatus: upload speed in mbps
|
||||
- pingStatus: ping in ms
|
||||
- jitterStatus: jitter in ms
|
||||
- dlProgress: progress of the download test as a float 0-1
|
||||
@ -49,7 +49,7 @@ function Speedtest() {
|
||||
this._settings = {}; //settings for the speedtest worker
|
||||
this._state = 0; //0=adding settings, 1=adding servers, 2=server selection done, 3=test running, 4=done
|
||||
console.log(
|
||||
"LibreSpeed by Federico Dossena v5.2 - https://github.com/librespeed/speedtest"
|
||||
"LibreSpeed by Federico Dossena v5.2.4 - https://github.com/librespeed/speedtest"
|
||||
);
|
||||
}
|
||||
|
||||
@ -69,10 +69,10 @@ Speedtest.prototype = {
|
||||
* Invalid values or nonexistant parameters will be ignored by the speedtest worker.
|
||||
*/
|
||||
setParameter: function(parameter, value) {
|
||||
if (this._state != 0)
|
||||
throw "You cannot change the test settings after adding server or starting the test";
|
||||
if (this._state == 3)
|
||||
throw "You cannot change the test settings while running the test";
|
||||
this._settings[parameter] = value;
|
||||
if(parameter === "temeletry_extra"){
|
||||
if(parameter === "telemetry_extra"){
|
||||
this._originalExtra=this._settings.telemetry_extra;
|
||||
}
|
||||
},
|
||||
@ -191,9 +191,9 @@ Speedtest.prototype = {
|
||||
throw "You can't select a server while the test is running";
|
||||
}
|
||||
if (this._selectServerCalled) throw "selectServer already called"; else this._selectServerCalled=true;
|
||||
/*this function goes through a list of servers. For each server, the ping is measured, then the server with the function result is called with the best server, or null if all the servers were down.
|
||||
/*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, result) {
|
||||
var select = function(serverList, selected) {
|
||||
//pings the specified URL, then calls the function result. Result will receive a parameter which is either the time it took to ping the URL, or -1 if something went wrong.
|
||||
var PING_TIMEOUT = 2000;
|
||||
var USE_PING_TIMEOUT = true; //will be disabled on unsupported browsers
|
||||
@ -201,7 +201,7 @@ Speedtest.prototype = {
|
||||
//IE11 doesn't support XHR timeout
|
||||
USE_PING_TIMEOUT = false;
|
||||
}
|
||||
var ping = function(url, result) {
|
||||
var ping = function(url, rtt) {
|
||||
url += (url.match(/\?/) ? "&" : "?") + "cors=true";
|
||||
var xhr = new XMLHttpRequest();
|
||||
var t = new Date().getTime();
|
||||
@ -217,11 +217,11 @@ Speedtest.prototype = {
|
||||
if (d <= 0) d = p.duration;
|
||||
if (d > 0 && d < instspd) instspd = d;
|
||||
} catch (e) {}
|
||||
result(instspd);
|
||||
} else result(-1);
|
||||
rtt(instspd);
|
||||
} else rtt(-1);
|
||||
}.bind(this);
|
||||
xhr.onerror = function() {
|
||||
result(-1);
|
||||
rtt(-1);
|
||||
}.bind(this);
|
||||
xhr.open("GET", url);
|
||||
if (USE_PING_TIMEOUT) {
|
||||
@ -271,7 +271,7 @@ Speedtest.prototype = {
|
||||
)
|
||||
bestServer = serverList[i];
|
||||
}
|
||||
result(bestServer);
|
||||
selected(bestServer);
|
||||
}.bind(this);
|
||||
var nextServer = function() {
|
||||
if (i == serverList.length) {
|
||||
@ -330,13 +330,13 @@ Speedtest.prototype = {
|
||||
console.error("Speedtest onupdate event threw exception: " + e);
|
||||
}
|
||||
if (data.testState >= 4) {
|
||||
clearInterval(this.updater);
|
||||
this._state = 4;
|
||||
try {
|
||||
if (this.onend) this.onend(data.testState == 5);
|
||||
} catch (e) {
|
||||
console.error("Speedtest onend event threw exception: " + e);
|
||||
}
|
||||
clearInterval(this.updater);
|
||||
this._state = 4;
|
||||
}
|
||||
}.bind(this);
|
||||
this.updater = setInterval(
|
29
assets/speedtest_worker.js → web/assets/speedtest_worker.js
Normal file → Executable file
29
assets/speedtest_worker.js → web/assets/speedtest_worker.js
Normal file → Executable file
@ -7,11 +7,11 @@
|
||||
|
||||
// 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 Mbit/s with 2 decimal digits
|
||||
var ulStatus = ""; // upload speed in Mbit/s with 2 decimal digits
|
||||
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
|
||||
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
|
||||
@ -45,10 +45,10 @@ var settings = {
|
||||
time_ulGraceTime: 3, //time to wait in seconds before actually measuring ul speed (wait for buffers to fill)
|
||||
time_dlGraceTime: 1.5, //time to wait in seconds before actually measuring dl speed (wait for TCP window to increase)
|
||||
count_ping: 10, // number of pings to perform in ping test
|
||||
url_dl: "backend/garbage", // path to a large file or garbage.php, used for download test. must be relative to this js file
|
||||
url_ul: "backend/empty", // path to an empty file, used for upload test. must be relative to this js file
|
||||
url_ping: "backend/empty", // path to an empty file, used for ping test. must be relative to this js file
|
||||
url_getIp: "backend/getIP", // path to getIP.php relative to this js file, or a similar thing that outputs the client's ip
|
||||
url_dl: "backend/garbage.php", // path to a large file or garbage.php, used for download test. must be relative to this js file
|
||||
url_ul: "backend/empty.php", // path to an empty file, used for upload test. must be relative to this js file
|
||||
url_ping: "backend/empty.php", // path to an empty file, used for ping test. must be relative to this js file
|
||||
url_getIp: "backend/getIP.php", // path to getIP.php relative to this js file, or a similar thing that outputs the client's ip
|
||||
getIp_ispInfo: true, //if set to true, the server will include ISP info with the IP address
|
||||
getIp_ispInfo_distance: "km", //km or mi=estimate distance from server in km/mi; set to false to disable distance estimation. getIp_ispInfo must be enabled in order for this to work
|
||||
xhr_dlMultistream: 6, // number of download streams to use (can be different if enable_quirks is active)
|
||||
@ -61,10 +61,11 @@ var settings = {
|
||||
enable_quirks: true, // enable quirks for specific browsers. currently it overrides settings to optimize for specific browsers, unless they are already being overridden with the start command
|
||||
ping_allowPerformanceApi: true, // if enabled, the ping test will attempt to calculate the ping more precisely using the Performance API. Currently works perfectly in Chrome, badly in Edge, and not at all in Firefox. If Performance API is not supported or the result is obviously wrong, a fallback is provided.
|
||||
overheadCompensationFactor: 1.06, //can be changed to compensatie for transport overhead. (see doc.md for some other values)
|
||||
useMebibits: false, //if set to true, speed will be reported in Mibit/s instead of Mbit/s
|
||||
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", // 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
|
||||
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
|
||||
};
|
||||
|
||||
var xhr = null; // array of currently active xhr requests
|
||||
@ -403,8 +404,8 @@ function dlTest(done) {
|
||||
var speed = totLoaded / (t / 1000.0);
|
||||
if (settings.time_auto) {
|
||||
//decide how much to shorten the test. Every 200ms, the test is shortened by the bonusT calculated here
|
||||
var bonus = (6.4 * speed) / 100000;
|
||||
bonusT += bonus > 800 ? 800 : bonus;
|
||||
var bonus = (5.0 * speed) / 100000;
|
||||
bonusT += bonus > 400 ? 400 : bonus;
|
||||
}
|
||||
//update status
|
||||
dlStatus = ((speed * 8 * settings.overheadCompensationFactor) / (settings.useMebibits ? 1048576 : 1000000)).toFixed(2); // speed is multiplied by 8 to go from bytes to bits, overhead compensation is applied, then everything is divided by 1048576 or 1000000 to go to megabits/mebibits
|
||||
@ -551,8 +552,8 @@ function ulTest(done) {
|
||||
var speed = totLoaded / (t / 1000.0);
|
||||
if (settings.time_auto) {
|
||||
//decide how much to shorten the test. Every 200ms, the test is shortened by the bonusT calculated here
|
||||
var bonus = (6.4 * speed) / 100000;
|
||||
bonusT += bonus > 800 ? 800 : bonus;
|
||||
var bonus = (5.0 * speed) / 100000;
|
||||
bonusT += bonus > 400 ? 400 : bonus;
|
||||
}
|
||||
//update status
|
||||
ulStatus = ((speed * 8 * settings.overheadCompensationFactor) / (settings.useMebibits ? 1048576 : 1000000)).toFixed(2); // speed is multiplied by 8 to go from bytes to bits, overhead compensation is applied, then everything is divided by 1048576 or 1000000 to go to megabits/mebibits
|
@ -68,7 +68,7 @@ func getIPInfo(addr string) results.IPInfoResponse {
|
||||
}
|
||||
|
||||
func SetServerLocation(conf *config.Config) {
|
||||
if conf.ServerLat > 0 && conf.ServerLng > 0 {
|
||||
if conf.ServerLat != 0 || conf.ServerLng != 0 {
|
||||
log.Infof("Configured server coordinates: %.6f, %.6f", conf.ServerLat, conf.ServerLng)
|
||||
serverCoord.Lat = conf.ServerLat
|
||||
serverCoord.Lon = conf.ServerLng
|
||||
|
42
web/listener.go
Normal file
42
web/listener.go
Normal file
@ -0,0 +1,42 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/librespeed/speedtest/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func startListener(conf *config.Config, r *chi.Mux) error {
|
||||
var s error
|
||||
|
||||
addr := net.JoinHostPort(conf.BindAddress, conf.Port)
|
||||
log.Infof("Starting backend server on %s", addr)
|
||||
|
||||
// TLS and HTTP/2.
|
||||
if conf.EnableTLS {
|
||||
log.Info("Use TLS connection.")
|
||||
if !(conf.EnableHTTP2) {
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: r,
|
||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
||||
}
|
||||
s = srv.ListenAndServeTLS(conf.TLSCertFile, conf.TLSKeyFile)
|
||||
} else {
|
||||
s = http.ListenAndServeTLS(addr, conf.TLSCertFile, conf.TLSKeyFile, r)
|
||||
}
|
||||
} else {
|
||||
if conf.EnableHTTP2 {
|
||||
log.Errorf("TLS is mandatory for HTTP/2. Ignore settings that enable HTTP/2.")
|
||||
}
|
||||
s = http.ListenAndServe(addr, r)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
60
web/listener_linux.go
Normal file
60
web/listener_linux.go
Normal file
@ -0,0 +1,60 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"github.com/coreos/go-systemd/v22/activation"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/librespeed/speedtest/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func startListener(conf *config.Config, r *chi.Mux) error {
|
||||
// See if systemd socket activation has been used when starting our process
|
||||
listeners, err := activation.Listeners()
|
||||
if err != nil {
|
||||
log.Fatalf("Error whilst checking for systemd socket activation %s", err)
|
||||
}
|
||||
|
||||
var s error
|
||||
|
||||
switch len(listeners) {
|
||||
case 0:
|
||||
addr := net.JoinHostPort(conf.BindAddress, conf.Port)
|
||||
log.Infof("Starting backend server on %s", addr)
|
||||
|
||||
// TLS and HTTP/2.
|
||||
if conf.EnableTLS {
|
||||
log.Info("Use TLS connection.")
|
||||
if !(conf.EnableHTTP2) {
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: r,
|
||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
||||
}
|
||||
s = srv.ListenAndServeTLS(conf.TLSCertFile, conf.TLSKeyFile)
|
||||
} else {
|
||||
s = http.ListenAndServeTLS(addr, conf.TLSCertFile, conf.TLSKeyFile, r)
|
||||
}
|
||||
} else {
|
||||
if conf.EnableHTTP2 {
|
||||
log.Errorf("TLS is mandatory for HTTP/2. Ignore settings that enable HTTP/2.")
|
||||
}
|
||||
s = http.ListenAndServe(addr, r)
|
||||
}
|
||||
case 1:
|
||||
log.Info("Starting backend server on inherited file descriptor via systemd socket activation")
|
||||
if conf.BindAddress != "" || conf.Port != "" {
|
||||
log.Errorf("Both an address/port (%s:%s) has been specificed in the config AND externally configured socket activation has been detected", conf.BindAddress, conf.Port)
|
||||
log.Fatal(`Please deconfigure socket activation (e.g. in systemd unit files), or set both 'bind_address' and 'listen_port' to ''`)
|
||||
}
|
||||
s = http.Serve(listeners[0], r)
|
||||
default:
|
||||
log.Fatalf("Asked to listen on %d sockets via systemd activation. Sorry we currently only support listening on 1 socket.", len(listeners))
|
||||
}
|
||||
return s
|
||||
}
|
94
web/web.go
94
web/web.go
@ -1,11 +1,14 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -26,6 +29,9 @@ const (
|
||||
chunkSize = 1048576
|
||||
)
|
||||
|
||||
//go:embed assets
|
||||
var defaultAssets embed.FS
|
||||
|
||||
var (
|
||||
// generate random data for download test on start to minimize runtime overhead
|
||||
randomData = getRandomData(chunkSize)
|
||||
@ -46,38 +52,49 @@ func ListenAndServe(conf *config.Config) error {
|
||||
r.Use(middleware.NoCache)
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
addr := net.JoinHostPort(conf.BindAddress, conf.Port)
|
||||
log.Infof("Starting backend server on %s", addr)
|
||||
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)
|
||||
var assetFS http.FileSystem
|
||||
if fi, err := os.Stat(conf.AssetsPath); os.IsNotExist(err) || !fi.IsDir() {
|
||||
log.Warnf("Configured asset path %s does not exist or is not a directory, using default assets", conf.AssetsPath)
|
||||
sub, err := fs.Sub(defaultAssets, "assets")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed when processing default assets: %s", err)
|
||||
}
|
||||
assetFS = http.FS(sub)
|
||||
} else {
|
||||
assetFS = justFilesFilesystem{fs: http.Dir(conf.AssetsPath), readDirBatchSize: 2}
|
||||
}
|
||||
|
||||
r.Get(conf.BaseURL+"/*", pages(assetFS, conf.BaseURL))
|
||||
r.HandleFunc(conf.BaseURL+"/empty", empty)
|
||||
r.HandleFunc(conf.BaseURL+"/backend/empty", empty)
|
||||
r.Get(conf.BaseURL+"/garbage", garbage)
|
||||
r.Get(conf.BaseURL+"/backend/garbage", garbage)
|
||||
r.Get(conf.BaseURL+"/getIP", getIP)
|
||||
r.Get(conf.BaseURL+"/backend/getIP", getIP)
|
||||
r.Get(conf.BaseURL+"/results", results.DrawPNG)
|
||||
r.Get(conf.BaseURL+"/results/", results.DrawPNG)
|
||||
r.Get(conf.BaseURL+"/backend/results", results.DrawPNG)
|
||||
r.Get(conf.BaseURL+"/backend/results/", results.DrawPNG)
|
||||
r.Post(conf.BaseURL+"/results/telemetry", results.Record)
|
||||
r.Post(conf.BaseURL+"/backend/results/telemetry", results.Record)
|
||||
r.HandleFunc(conf.BaseURL+"/stats", results.Stats)
|
||||
r.HandleFunc(conf.BaseURL+"/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)
|
||||
r.HandleFunc(conf.BaseURL+"/empty.php", empty)
|
||||
r.HandleFunc(conf.BaseURL+"/backend/empty.php", empty)
|
||||
r.Get(conf.BaseURL+"/garbage.php", garbage)
|
||||
r.Get(conf.BaseURL+"/backend/garbage.php", garbage)
|
||||
r.Get(conf.BaseURL+"/getIP.php", getIP)
|
||||
r.Get(conf.BaseURL+"/backend/getIP.php", getIP)
|
||||
r.Post(conf.BaseURL+"/results/telemetry.php", results.Record)
|
||||
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)
|
||||
|
||||
go listenProxyProtocol(conf, r)
|
||||
return http.ListenAndServe(addr, r)
|
||||
|
||||
return startListener(conf, r)
|
||||
}
|
||||
|
||||
func listenProxyProtocol(conf *config.Config, r *chi.Mux) {
|
||||
@ -96,14 +113,23 @@ func listenProxyProtocol(conf *config.Config, r *chi.Mux) {
|
||||
}
|
||||
}
|
||||
|
||||
func pages(w http.ResponseWriter, r *http.Request) {
|
||||
if r.RequestURI == "/" {
|
||||
r.RequestURI = "/index.html"
|
||||
func pages(fs http.FileSystem, BaseURL string) http.HandlerFunc {
|
||||
var removeBaseURL *regexp.Regexp
|
||||
if BaseURL != "" {
|
||||
removeBaseURL = regexp.MustCompile("^" + BaseURL + "/")
|
||||
}
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
if BaseURL != "" {
|
||||
r.URL.Path = removeBaseURL.ReplaceAllString(r.URL.Path, "/")
|
||||
}
|
||||
if r.RequestURI == "/" {
|
||||
r.RequestURI = "/index.html"
|
||||
}
|
||||
|
||||
http.FileServer(fs).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
conf := config.LoadedConfig()
|
||||
fs := justFilesFilesystem{fs: http.Dir(conf.AssetsPath), readDirBatchSize: 2}
|
||||
http.FileServer(fs).ServeHTTP(w, r)
|
||||
return fn
|
||||
}
|
||||
|
||||
func empty(w http.ResponseWriter, r *http.Request) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user