42 Commits

Author SHA1 Message Date
balki 0f5f67cd4b revert module name 2026-05-07 14:03:22 -04:00
balki 4e84182926 Add bcrypt support for stats password 2026-05-07 14:00:40 -04:00
balki b7c5835680 Fix initialization of conf for stats 2026-05-07 13:59:17 -04:00
Maddie Zhan 7bbfadeeed fix: skip upx for darwin/arm64 target 2026-04-30 14:26:02 +08:00
Maddie Zhan 7ff3ef4bfb fix: remove unsupported archs on linux platform 2026-04-30 14:23:29 +08:00
Maddie Zhan f0c79fd6f1 fix: remove unsupported archs on linux platform 2026-04-30 14:19:59 +08:00
Maddie Zhan 5c33d51392 fix: remove unsupported archs on freebsd platform 2026-04-30 14:16:45 +08:00
Maddie Zhan c9cbed8313 chore: migrate goreleaser config to v2 2026-04-30 14:09:01 +08:00
Maddie Zhan cd20f44d20 Sync PHP backend feature parity: IP detection, database backends, API endpoints, and frontend
- IP detection: Cloudflare IPv6, ULA IPv6, proxy header chain, offline GeoIP DB
- Database: add SQLite (pure Go, no CGo) and MSSQL backends
- API: add JSON result sharing endpoint and ID obfuscation
- Frontend: add modern CSS design, design switcher, favicon
- Compatibility: ?cors parameter support, human-friendly distance rounding
- Update Go to 1.21, add modernc.org/sqlite and maxminddb deps
2026-04-30 13:53:52 +08:00
Michael Stapelberg 603cbdeec5 go.mod: make module name match repository (#74) 2026-02-25 18:32:44 +08:00
风起 123cd29ffb fix document bug (#71) 2026-02-25 18:31:10 +08:00
Qasim Mehmood faa59b8261 Faster multi stage build using cross compilation (#72)
Co-authored-by: maddie <maddie@users.noreply.github.com>
2026-02-25 18:30:21 +08:00
Cobalt aba2be835f docker: use scratch for runtime image (#77) 2026-02-25 18:27:10 +08:00
Nick Miller 553a8c18cc Fix IP redaction: assign ReplaceAllString return values (#79)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 18:26:50 +08:00
mickkael 7001fa4fa5 Create arm-registry.yml (#58) 2023-04-11 13:51:41 +08:00
Devin Buhl fa28701928 feat: add env configuration support (#57) 2023-04-11 13:51:29 +08:00
adamus1red 4afbe82283 Add docker multi-arch build (#55)
Add buildx & qemu for generating the multi-arch digest
Add builds for platforms amd64, arm64, Armv7 (Raspberry Pi 32bit), and 386
2023-04-11 13:50:59 +08:00
Tim Small a3eec03845 Bump go-systemd from unreleased git version to release v22.4.0 (#54) 2023-04-11 13:50:39 +08:00
Sharl.Jimh.Tsin 2ea9df0aba fix cookies not match while using subPath (#51) 2023-04-11 13:50:20 +08:00
minus 8e31fe25ac Make Docker image easier to use (#50)
* Use single server example as default frontend

This just works out of the box

* Refactor Dockerfile

- Remove the asset dir since it's embedded in the executable anyway
- Use the code in the current directory to build instead of downloading.
  Makes local testing very easy.
- Cache Go dependencies
- Run without root privileges. Note that using a BoltDB database
  requires mounting a volume with the correct permissions. The default
  database has thus been changed to memory so that starting the image
  just works.

* Automatically publish images
2022-07-26 23:30:23 +08:00
kayos 580fc08e0e Fix wrong format type in Fatalf call (#48)
> `web/listener_linux.go:57:3: github.com/sirupsen/logrus.Fatalf format %s has arg len(listeners) of wrong type int`

for ints we use %d
2022-07-23 20:20:28 +08:00
Maddie Zhan 9cbc95a6db Fix Windows build
Fixes #44, closes #45
2022-07-09 01:03:33 +08:00
Maddie Zhan fa749f3b45 goreleaser: don't upx compress Linux MIPS bianries 2022-07-09 00:54:04 +08:00
Nicolas Ledez a5d18ef24c Added url_base parameter to customize the root url and allow hosting with another application. (#41)
* Added `url_base` parameter and code to rewrite URL

* Manage redirect on stats login

Co-authored-by: Nicolas Ledez <github.public@ledez.net>
2022-07-09 00:44:49 +08:00
Marc f09f2df2e3 Changed installation guide (#40)
- Added a notice about install the software using prebuilt binary packages.
- Added a notice about installing the software using my Ansible role.
2022-07-09 00:44:00 +08:00
yas-nyan 86763bd3aa Feature: TLS and HTTP/2 (#39) 2022-03-27 02:47:16 +08:00
Maddie Zhan 22f21a270b Add missing go.mod dependency 2022-01-18 15:52:20 +08:00
kaustubh105 5944c3ca28 Rework session management (#37)
Co-authored-by: Kaustubh <kaustubh@rop.app>
2022-01-18 15:48:01 +08:00
Tim Small c67b8ec91d Support systemd socket activation (#35)
* Support systemd socket activation

If the process has been started with systemd socket activation
configured, then serve requests on the passed-in socket instead of
attempting to bind to an address.

* Add example systemd unit files.

Add example systemd unit files which make use of systemd's security
facilities, and also allow binding to port 80 whilst running as an
unpriviliged process (using systemd socket activation).
2022-01-18 15:47:32 +08:00
Sean McAvoy 281aff1725 Update Dockerfile (#36)
update alpine to 3.15 (same as golang:alpine)
2022-01-18 15:46:15 +08:00
Axel Gembe 158e37d3ae Docker: Fix asset copy step to copy from the correct directory (#33)
Assets were moved from the `assets` folder to the `web/assets` folder in
commit 7204ae2e19 but the `Dockerfile` has
not been adjusted to copy from there.
2021-10-17 21:51:13 +08:00
Maddie Zhan 2583e5113a Use default html assets 2021-09-27 11:11:08 +08:00
Maddie Zhan 42cc7740a5 Fix windows/arm64 build 2021-09-18 13:56:26 +08:00
Maddie Zhan bee29e8b7e Update goreleaser config 2021-09-18 13:12:01 +08:00
Maddie Zhan 89a5de0105 Support in-memory telemetry/stats database 2021-09-18 02:19:14 +08:00
Maddie Zhan 800760054e Embed tzdata and rootcerts in binary
For use in environments without tzdata or root certs (like embedded devices)

Fixes #29
2021-09-18 01:58:35 +08:00
Maddie Zhan 8c1aa6b39b Support none database type to disable stats/telemetry
Fixes #21
2021-09-18 01:48:10 +08:00
Maddie Zhan 7204ae2e19 Embed default assets 2021-09-17 21:06:00 +08:00
Maddie Zhan 83d25e00fe Update dependencies 2021-09-17 20:32:57 +08:00
Maddie Zhan a9adc15ce9 Sync frontend components with PHP version 2021-09-17 20:32:31 +08:00
udarnik386 2ca5dbf071 Add RPM package SPEC (#23) 2021-07-05 14:02:19 +08:00
Maddie Zhan dcb9882978 Allow negative coordinates
Fixes #25
2021-05-21 14:21:45 +08:00
73 changed files with 4255 additions and 1218 deletions
+47
View 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
View 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 }}
+77 -77
View File
@@ -1,96 +1,96 @@
version: 2
project_name: 'speedtest-go' project_name: 'speedtest-go'
#dist: ./out #dist: ./out
before: before:
hooks: hooks:
- go mod download - go mod download
builds: builds:
- main: ./main.go - main: ./main.go
id: speedtest-backend id: speedtest-backend
binary: speedtest-backend binary: speedtest-backend
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
flags: flags:
- -trimpath - -trimpath
ldflags: ldflags:
- -w -s - -w -s
goos: goos:
- windows - windows
- linux - linux
- darwin goarch:
goarch: - 386
- 386 - amd64
- amd64 - arm
- arm - arm64
- arm64 goarm:
- mips - 5
- mipsle - 6
goarm: - 7
- 5 ignore:
- 6 - goos: windows
- 7 goarch: arm
gomips: - goos: windows
- hardfloat goarch: arm64
- softfloat hooks:
ignore: post: upx -9 "{{ .Path }}"
- goos: darwin - main: ./main.go
goarch: 386 id: speedtest-backend-darwin
hooks: binary: speedtest-backend
post: upx -9 "{{ .Path }}" env:
- main: ./main.go - CGO_ENABLED=0
id: speedtest-backend-freebsd flags:
binary: speedtest-backend - -trimpath
env: ldflags:
- CGO_ENABLED=0 - -w -s
flags: goos:
- -trimpath - darwin
ldflags: goarch:
- -w -s - amd64
goos: - arm64
- freebsd - main: ./main.go
goarch: id: speedtest-backend-freebsd
- 386 binary: speedtest-backend
- amd64 env:
- arm - CGO_ENABLED=0
- arm64 flags:
- mips - -trimpath
- mipsle ldflags:
goarm: - -w -s
- 5 goos:
- 6 - freebsd
- 7 goarch:
gomips: - amd64
- hardfloat - arm64
- softfloat - main: ./main.go
- main: ./main.go id: speedtest-backend-noupx-windows-arm64
id: speedtest-backend-noupx binary: speedtest-backend
binary: speedtest-backend env:
env: - CGO_ENABLED=0
- CGO_ENABLED=0 flags:
flags: - -trimpath
- -trimpath ldflags:
ldflags: - -w -s
- -w -s goos:
goos: - windows
- linux goarch:
goarch: - arm
- mips64 - arm64
- mips64le goarm:
gomips: - 5
- hardfloat - 6
- softfloat - 7
archives: archives:
- format_overrides: - format_overrides:
- goos: windows - goos: windows
format: zip formats: zip
files: files:
- README.md - README.md
- LICENSE - LICENSE
- assets/*
- settings.toml - settings.toml
checksum: checksum:
name_template: 'checksums.txt' name_template: 'checksums.txt'
changelog: changelog:
skip: false disable: false
sort: asc sort: asc
release: release:
github: github:
+13 -14
View File
@@ -1,19 +1,18 @@
FROM golang:alpine AS build_base FROM --platform=$BUILDPLATFORM golang:1.18-alpine AS build_base
#ENV GOARCH arm64 RUN apk add --no-cache git gcc ca-certificates libc-dev
#ENV GOARCH amd64 WORKDIR /build
RUN apk add --no-cache git gcc ca-certificates libc-dev \ COPY go.mod go.sum ./
&& mkdir -p /go/src/github.com/librespeed/ \ RUN go mod download
&& cd /go/src/github.com/librespeed/ \ COPY ./ ./
&& 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 alpine:3.9 ENV CGO_ENABLED=0
RUN apk add ca-certificates ARG TARGETOS TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -trimpath -buildvcs=false -o speedtest .
FROM scratch
WORKDIR /app WORKDIR /app
COPY --from=build_base /go/src/github.com/librespeed/speedtest-go/speedtest . COPY --from=build_base /build/speedtest ./
COPY --from=build_base /go/src/github.com/librespeed/speedtest-go/assets ./assets COPY settings.toml ./
COPY --from=build_base /go/src/github.com/librespeed/speedtest-go/settings.toml .
EXPOSE 8989 EXPOSE 8989
+54 -23
View File
@@ -20,36 +20,48 @@ Works with mobile versions too.
* Jitter * Jitter
* IP Address, ISP, distance from server (optional) * IP Address, ISP, distance from server (optional)
* Telemetry (optional) * Telemetry (optional)
* Results sharing (optional) * Results sharing via PNG image and JSON API (optional)
* Multiple Points of Test (optional) * Multiple Points of Test (optional)
* Compatible with PHP frontend predefined endpoints (with `.php` suffixes) * Compatible with PHP frontend predefined endpoints (with `.php` suffixes)
* Supports [Proxy Protocol](https://www.haproxy.org/download/2.3/doc/proxy-protocol.txt) (without TLV support yet) * Supports [Proxy Protocol](https://www.haproxy.org/download/2.3/doc/proxy-protocol.txt)
* Modern and classic UI designs with switchable interface
* ID obfuscation for test result privacy (optional)
### IP Detection
* Client IP detection with proxy header chain support (X-Forwarded-For, X-Real-IP, Client-IP, CF-Connecting-IPv6)
* ISP and location detection via ipinfo.io API with offline GeoIP database fallback (MaxMind .mmdb)
* Private/special IP detection (including ULA IPv6 and CGNAT)
* Distance calculation with human-friendly rounding
![Screencast](https://speedtest.zzz.cat/speedtest.webp) ![Screencast](https://speedtest.zzz.cat/speedtest.webp)
## Server requirements ## Server requirements
* Any [Go supported platforms](https://github.com/golang/go/wiki/MinimumRequirements) * Any [Go supported platforms](https://github.com/golang/go/wiki/MinimumRequirements) (Go 1.21+)
* BoltDB, PostgreSQL or MySQL database to store test results (optional) * SQLite, BoltDB, PostgreSQL, MySQL or MSSQL database to store test results (optional)
* No external dependencies — single binary deployment
* A fast! Internet connection * A fast! Internet connection
## Installation ## 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
manually, you can install newer version of Go into your `GOPATH`:
0. Install Go 1.14 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
$ go get golang.org/dl/go1.14.2
# Assuming your GOPATH is default (~/go), Go 1.14.2 will be installed in ~/go/bin 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.
$ ~/go/bin/go1.14.2 version ### Compile from source
go version go1.14.2 linux/amd64
``` You need Go 1.21+ to compile the binary.
1. Clone this repository: 1. Clone this repository:
``` ```
$ git clone github.com/librespeed/speedtest-go $ git clone https://github.com/librespeed/speedtest-go
``` ```
2. Build 2. Build
@@ -63,19 +75,21 @@ manually, you can install newer version of Go into your `GOPATH`:
3. Copy the `assets` directory, `settings.toml` file along with the compiled `speedtest` binary into a single directory 3. Copy the `assets` directory, `settings.toml` file along with the compiled `speedtest` binary into a single directory
4. If you have telemetry enabled, 4. If you have telemetry enabled,
- For PostgreSQL/MySQL, create database and import the corresponding `.sql` file under `database/{postgresql,mysql}` - For PostgreSQL/MySQL/MSSQL, create database and import the corresponding `.sql` file under `database/{postgresql,mysql,mssql}`
``` ```
# assume you have already created a database named `speedtest` under current user # assume you have already created a database named `speedtest` under current user
$ psql speedtest < database/postgresql/telemetry_postgresql.sql $ psql speedtest < database/postgresql/telemetry_postgresql.sql
``` ```
- For embedded BoltDB, make sure to define the `database_file` path in `settings.toml`: - For embedded databases (BoltDB, SQLite), make sure to define the `database_file` path in `settings.toml`:
``` ```
database_file="speedtest.db" database_file="speedtest.db"
``` ```
- SQLite supports WAL mode for better concurrent performance and works out of the box with no additional dependencies.
5. Put `assets` folder under the same directory as your compiled binary. 5. Put `assets` folder under the same directory as your compiled binary.
- Make sure the font files and JavaScripts are in the `assets` directory - Make sure the font files and JavaScripts are in the `assets` directory
- You can have multiple HTML pages under `assets` directory. They can be access directly under the server root - You can have multiple HTML pages under `assets` directory. They can be access directly under the server root
@@ -98,6 +112,7 @@ manually, you can install newer version of Go into your `GOPATH`:
ipinfo_api_key="" ipinfo_api_key=""
# assets directory path, defaults to `assets` in the same directory # 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" assets_path="./assets"
# password for logging into statistics page, change this to enable stats page # password for logging into statistics page, change this to enable stats page
@@ -105,24 +120,40 @@ manually, you can install newer version of Go into your `GOPATH`:
# redact IP addresses # redact IP addresses
redact_ip_addresses=false redact_ip_addresses=false
# database type for statistics data, currently supports: bolt, mysql, postgresql # database type for statistics data, currently supports: none, memory, bolt, sqlite, mysql, postgresql, mssql
# if none is specified, no telemetry/stats will be recorded, and no result PNG will be generated
database_type="postgresql" database_type="postgresql"
database_hostname="localhost" database_hostname="localhost"
database_name="speedtest" database_name="speedtest"
database_username="postgres" database_username="postgres"
database_password="" database_password=""
# if you use `bolt` as database, set database_file to database file location # database port (optional, defaults to driver default; only used by mssql)
database_port=""
# if you use `bolt` or `sqlite` as database, set database_file to database file location
database_file="speedtest.db" database_file="speedtest.db"
# GeoIP offline database (.mmdb format) for ISP detection fallback (optional)
# Leave empty to disable.
# geoip_database_file="country_asn.mmdb"
# 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 ## Differences between Go and PHP implementation and caveats
- Since there is no CGo-free SQLite implementation available, I've opted to use [BoltDB](https://github.com/etcd-io/bbolt) - Test IDs are generated as ULID (Universally Unique Lexicographically Sortable Identifier), unlike the PHP version's auto-increment integer IDs
instead, as an embedded database alternative to SQLite - ID obfuscation is available as an optional feature — when enabled, ULIDs are obfuscated with a per-instance salt
- Test IDs are generated ULID, there is no option to change them to plain ID - The Go version ships with two built-in UI designs (classic gauges and modern CSS), switchable via `?design=new` URL parameter
- You can use the same HTML template from the PHP implementation - The modern design (`index-modern.html`) supports multi-server configuration via `server-list.json` placed alongside the binary
- Server location can be defined in settings - Server location can be defined in settings or auto-detected at startup
- There might be a slight delay on program start if your Internet connection is slow. That's because the program will - There might be a slight delay on program start if your Internet connection is slow. That's because the program will
attempt to fetch your current network's ISP info for distance calculation between your network and the speed test client's. attempt to fetch your current network's ISP info for distance calculation between your network and the speed test client's.
This action will only be taken once, and cached for later use. This action will only be taken once, and cached for later use.
-365
View File
@@ -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>
+19 -12
View File
@@ -8,13 +8,15 @@ import (
type Config struct { type Config struct {
BindAddress string `mapstructure:"bind_address"` BindAddress string `mapstructure:"bind_address"`
Port string `mapstructure:"listen_port"` Port string `mapstructure:"listen_port"`
BaseURL string `mapstructure:"url_base"`
ProxyProtocolPort string `mapstructure:"proxyprotocol_port"` ProxyProtocolPort string `mapstructure:"proxyprotocol_port"`
ServerLat float64 `mapstructure:"server_lat"` ServerLat float64 `mapstructure:"server_lat"`
ServerLng float64 `mapstructure:"server_lng"` ServerLng float64 `mapstructure:"server_lng"`
IPInfoAPIKey string `mapstructure:"ipinfo_api_key"` IPInfoAPIKey string `mapstructure:"ipinfo_api_key"`
StatsPassword string `mapstructure:"statistics_password"` StatsPassword string `mapstructure:"statistics_password"`
RedactIP bool `mapstructure:"redact_ip_addresses"` RedactIP bool `mapstructure:"redact_ip_addresses"`
EnableIDObfuscation bool `mapstructure:"enable_id_obfuscation"`
AssetsPath string `mapstructure:"assets_path"` AssetsPath string `mapstructure:"assets_path"`
@@ -25,26 +27,36 @@ type Config struct {
DatabasePassword string `mapstructure:"database_password"` DatabasePassword string `mapstructure:"database_password"`
DatabaseFile string `mapstructure:"database_file"` DatabaseFile string `mapstructure:"database_file"`
DatabasePort string `mapstructure:"database_port"`
GeoIPDatabaseFile string `mapstructure:"geoip_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 ( var (
configFile string = "" configFile string
loadedConfig *Config = nil loadedConfig *Config = nil
) )
func init() { func init() {
viper.SetDefault("listen_port", "8989") viper.SetDefault("listen_port", "8989")
viper.SetDefault("url_base", "")
viper.SetDefault("proxyprotocol_port", "0") viper.SetDefault("proxyprotocol_port", "0")
viper.SetDefault("download_chunks", 4) viper.SetDefault("download_chunks", 4)
viper.SetDefault("distance_unit", "K") viper.SetDefault("distance_unit", "K")
viper.SetDefault("enable_cors", false) viper.SetDefault("enable_cors", false)
viper.SetDefault("statistics_password", "PASSWORD") viper.SetDefault("statistics_password", "PASSWORD")
viper.SetDefault("redact_ip_addresses", false) viper.SetDefault("redact_ip_addresses", false)
viper.SetDefault("assets_path", "./assets")
viper.SetDefault("database_type", "postgresql") viper.SetDefault("database_type", "postgresql")
viper.SetDefault("database_hostname", "localhost") viper.SetDefault("database_hostname", "localhost")
viper.SetDefault("database_name", "speedtest") viper.SetDefault("database_name", "speedtest")
viper.SetDefault("database_username", "postgres") viper.SetDefault("database_username", "postgres")
viper.SetDefault("enable_tls", false)
viper.SetDefault("enable_http2", false)
viper.SetConfigName("settings") viper.SetConfigName("settings")
viper.AddConfigPath(".") viper.AddConfigPath(".")
@@ -55,14 +67,9 @@ func Load(configPath string) Config {
configFile = configPath configFile = configPath
viper.SetConfigFile(configPath) viper.SetConfigFile(configPath)
viper.SetEnvPrefix("speedtest")
if err := viper.ReadInConfig(); err != nil { viper.AutomaticEnv()
if _, ok := err.(viper.ConfigFileNotFoundError); ok { viper.ReadInConfig()
log.Warnf("No config file found in search paths, using default values")
} else {
log.Fatalf("Error reading config: %s", err)
}
}
if err := viper.Unmarshal(&conf); err != nil { if err := viper.Unmarshal(&conf); err != nil {
log.Fatalf("Error parsing config: %s", err) log.Fatalf("Error parsing config: %s", err)
+2 -4
View File
@@ -5,12 +5,10 @@ import (
"errors" "errors"
"time" "time"
"go.etcd.io/bbolt" "github.com/librespeed/speedtest-go/database/schema"
"github.com/librespeed/speedtest/database/schema"
_ "github.com/go-sql-driver/mysql"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"go.etcd.io/bbolt"
) )
const ( const (
+21 -5
View File
@@ -1,11 +1,17 @@
package database package database
import ( import (
"github.com/librespeed/speedtest/config" "github.com/librespeed/speedtest-go/config"
"github.com/librespeed/speedtest/database/bolt" "github.com/librespeed/speedtest-go/database/bolt"
"github.com/librespeed/speedtest/database/mysql" "github.com/librespeed/speedtest-go/database/memory"
"github.com/librespeed/speedtest/database/postgresql" "github.com/librespeed/speedtest-go/database/mssql"
"github.com/librespeed/speedtest/database/schema" "github.com/librespeed/speedtest-go/database/mysql"
"github.com/librespeed/speedtest-go/database/none"
"github.com/librespeed/speedtest-go/database/postgresql"
"github.com/librespeed/speedtest-go/database/schema"
"github.com/librespeed/speedtest-go/database/sqlite"
log "github.com/sirupsen/logrus"
) )
var ( var (
@@ -26,5 +32,15 @@ func SetDBInfo(conf *config.Config) {
DB = mysql.Open(conf.DatabaseHostname, conf.DatabaseUsername, conf.DatabasePassword, conf.DatabaseName) DB = mysql.Open(conf.DatabaseHostname, conf.DatabaseUsername, conf.DatabasePassword, conf.DatabaseName)
case "bolt": case "bolt":
DB = bolt.Open(conf.DatabaseFile) DB = bolt.Open(conf.DatabaseFile)
case "sqlite":
DB = sqlite.Open(conf.DatabaseFile)
case "mssql":
DB = mssql.Open(conf.DatabaseHostname, conf.DatabaseUsername, conf.DatabasePassword, conf.DatabaseName, conf.DatabasePort)
case "memory":
DB = memory.Open("")
case "none":
DB = none.Open("")
default:
log.Fatalf("Unsupported database type: %s", conf.DatabaseType)
} }
} }
+51
View File
@@ -0,0 +1,51 @@
package memory
import (
"errors"
"sync"
"time"
"github.com/librespeed/speedtest-go/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
}
+81
View File
@@ -0,0 +1,81 @@
package mssql
import (
"database/sql"
"fmt"
"net/url"
"github.com/librespeed/speedtest-go/database/schema"
_ "github.com/denisenkom/go-mssqldb"
log "github.com/sirupsen/logrus"
)
type MSSQL struct {
db *sql.DB
}
func Open(hostname, username, password, database, port string) *MSSQL {
if port == "" {
port = "1433"
}
query := url.Values{}
query.Add("database", database)
connStr := fmt.Sprintf("sqlserver://%s:%s@%s:%s?%s",
url.QueryEscape(username),
url.QueryEscape(password),
hostname,
port,
query.Encode(),
)
conn, err := sql.Open("sqlserver", connStr)
if err != nil {
log.Fatalf("Cannot open MSSQL database: %s", err)
}
return &MSSQL{db: conn}
}
func (p *MSSQL) Insert(data *schema.TelemetryData) error {
stmt := `INSERT INTO speedtest_users (ip, ispinfo, extra, ua, lang, dl, ul, ping, jitter, log, uuid)
VALUES (@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11);`
_, err := p.db.Exec(stmt,
data.IPAddress, data.ISPInfo, data.Extra, data.UserAgent, data.Language,
data.Download, data.Upload, data.Ping, data.Jitter, data.Log, data.UUID)
return err
}
func (p *MSSQL) FetchByUUID(uuid string) (*schema.TelemetryData, error) {
var record schema.TelemetryData
row := p.db.QueryRow(`SELECT * FROM speedtest_users WHERE uuid = @p1`, uuid)
if row != nil {
var id int64
if err := row.Scan(&id, &record.Timestamp, &record.IPAddress, &record.ISPInfo, &record.Extra, &record.UserAgent, &record.Language, &record.Download, &record.Upload, &record.Ping, &record.Jitter, &record.Log, &record.UUID); err != nil {
return nil, fmt.Errorf("mssql fetch by uuid: %w", err)
}
}
return &record, nil
}
func (p *MSSQL) FetchLast100() ([]schema.TelemetryData, error) {
var records []schema.TelemetryData
rows, err := p.db.Query(`SELECT TOP 100 * FROM speedtest_users ORDER BY timestamp DESC;`)
if err != nil {
return nil, fmt.Errorf("mssql fetch last 100: %w", err)
}
if rows != nil {
defer rows.Close()
for rows.Next() {
var record schema.TelemetryData
var id int64
if err := rows.Scan(&id, &record.Timestamp, &record.IPAddress, &record.ISPInfo, &record.Extra, &record.UserAgent, &record.Language, &record.Download, &record.Upload, &record.Ping, &record.Jitter, &record.Log, &record.UUID); err != nil {
return nil, fmt.Errorf("mssql scan row: %w", err)
}
records = append(records, record)
}
}
return records, nil
}
+27
View File
@@ -0,0 +1,27 @@
--
-- MSSQL database schema for speedtest telemetry
--
CREATE TABLE [dbo].[speedtest_users](
[id] [bigint] IDENTITY(120,1) NOT NULL,
[timestamp] [datetime] NOT NULL,
[ip] [nvarchar](max) NOT NULL,
[ispinfo] [nvarchar](max) NULL,
[extra] [nvarchar](max) NULL,
[ua] [nvarchar](max) NOT NULL,
[lang] [nvarchar](max) NOT NULL,
[dl] [nvarchar](max) NULL,
[ul] [nvarchar](max) NULL,
[ping] [nvarchar](max) NULL,
[jitter] [nvarchar](max) NULL,
[log] [nvarchar](max) NULL,
[uuid] [nvarchar](max) NULL,
CONSTRAINT [PK_speedtest_users] PRIMARY KEY CLUSTERED
(
[id] ASC
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
ALTER TABLE [dbo].[speedtest_users] ADD CONSTRAINT [DF_speedtest_users_timestamp] DEFAULT (getdate()) FOR [timestamp]
GO
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"github.com/librespeed/speedtest/database/schema" "github.com/librespeed/speedtest-go/database/schema"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
+23
View File
@@ -0,0 +1,23 @@
package none
import (
"github.com/librespeed/speedtest-go/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
}
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"github.com/librespeed/speedtest/database/schema" "github.com/librespeed/speedtest-go/database/schema"
_ "github.com/lib/pq" _ "github.com/lib/pq"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
+95
View File
@@ -0,0 +1,95 @@
package sqlite
import (
"database/sql"
"fmt"
"github.com/librespeed/speedtest-go/database/schema"
_ "modernc.org/sqlite"
log "github.com/sirupsen/logrus"
)
type SQLite struct {
db *sql.DB
}
func Open(databaseFile string) *SQLite {
conn, err := sql.Open("sqlite", databaseFile)
if err != nil {
log.Fatalf("Cannot open SQLite database: %s", err)
}
// Enable WAL mode for better concurrent performance
if _, err := conn.Exec("PRAGMA journal_mode=WAL"); err != nil {
log.Warnf("Failed to set SQLite journal mode to WAL: %s", err)
}
// Create table if not exists (matching the PHP SQLite auto-creation behavior)
stmt := `CREATE TABLE IF NOT EXISTS speedtest_users (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
ip TEXT NOT NULL,
ispinfo TEXT,
extra TEXT,
ua TEXT NOT NULL,
lang TEXT NOT NULL,
dl TEXT,
ul TEXT,
ping TEXT,
jitter TEXT,
log TEXT,
uuid TEXT
);`
if _, err := conn.Exec(stmt); err != nil {
log.Fatalf("Failed to create speedtest_users table: %s", err)
}
return &SQLite{db: conn}
}
func (p *SQLite) Insert(data *schema.TelemetryData) error {
var existingID int
// Check for duplicate UUID first
err := p.db.QueryRow(`SELECT id FROM speedtest_users WHERE uuid = ?`, data.UUID).Scan(&existingID)
if err == nil {
// Record with this UUID already exists - skip insert
return nil
}
stmt := `INSERT INTO speedtest_users (ip, ispinfo, extra, ua, lang, dl, ul, ping, jitter, log, uuid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`
_, err = p.db.Exec(stmt, data.IPAddress, data.ISPInfo, data.Extra, data.UserAgent, data.Language, data.Download, data.Upload, data.Ping, data.Jitter, data.Log, data.UUID)
return err
}
func (p *SQLite) FetchByUUID(uuid string) (*schema.TelemetryData, error) {
var record schema.TelemetryData
row := p.db.QueryRow(`SELECT * FROM speedtest_users WHERE uuid = ?`, uuid)
if row != nil {
var id int
if err := row.Scan(&id, &record.Timestamp, &record.IPAddress, &record.ISPInfo, &record.Extra, &record.UserAgent, &record.Language, &record.Download, &record.Upload, &record.Ping, &record.Jitter, &record.Log, &record.UUID); err != nil {
return nil, fmt.Errorf("sqlite fetch by uuid: %w", err)
}
}
return &record, nil
}
func (p *SQLite) FetchLast100() ([]schema.TelemetryData, error) {
var records []schema.TelemetryData
rows, err := p.db.Query(`SELECT * FROM speedtest_users ORDER BY timestamp DESC LIMIT 100;`)
if err != nil {
return nil, fmt.Errorf("sqlite fetch last 100: %w", err)
}
if rows != nil {
defer rows.Close()
for rows.Next() {
var record schema.TelemetryData
var id int
if err := rows.Scan(&id, &record.Timestamp, &record.IPAddress, &record.ISPInfo, &record.Extra, &record.UserAgent, &record.Language, &record.Download, &record.Upload, &record.Ping, &record.Jitter, &record.Log, &record.UUID); err != nil {
return nil, fmt.Errorf("sqlite scan row: %w", err)
}
records = append(records, record)
}
}
return records, nil
}
+21
View File
@@ -0,0 +1,21 @@
--
-- SQLite database schema for speedtest telemetry
-- Auto-created by the sqlite backend if it doesn't exist.
-- This file is provided for reference / manual setup.
--
CREATE TABLE IF NOT EXISTS `speedtest_users` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`timestamp` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`ip` TEXT NOT NULL,
`ispinfo` TEXT,
`extra` TEXT,
`ua` TEXT NOT NULL,
`lang` TEXT NOT NULL,
`dl` TEXT,
`ul` TEXT,
`ping` TEXT,
`jitter` TEXT,
`log` TEXT,
`uuid` TEXT
);
+43 -21
View File
@@ -1,32 +1,54 @@
module github.com/librespeed/speedtest module github.com/librespeed/speedtest-go
go 1.13 go 1.25.0
require ( require (
github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/breml/rootcerts v0.2.1
github.com/go-chi/chi/v5 v5.0.1 github.com/coreos/go-systemd/v22 v22.4.0
github.com/go-chi/cors v1.1.1 github.com/denisenkom/go-mssqldb v0.12.3
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-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/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect github.com/gorilla/securecookie v1.1.1
github.com/lib/pq v1.10.0 github.com/gorilla/sessions v1.2.1
github.com/magiconair/properties v1.8.4 // indirect github.com/lib/pq v1.10.4
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/oklog/ulid/v2 v2.0.2 github.com/oklog/ulid/v2 v2.0.2
github.com/pelletier/go-toml v1.8.1 // indirect github.com/oschwald/maxminddb-golang v1.13.1
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/sirupsen/logrus v1.8.1
github.com/spf13/afero v1.5.1 // indirect github.com/spf13/viper v1.10.1
github.com/spf13/cast v1.3.1 // indirect github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26
go.etcd.io/bbolt v1.3.6
golang.org/x/crypto v0.48.0
golang.org/x/image v0.0.0-20211028202545-6944b10bf410
modernc.org/sqlite v1.50.0
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/spf13/afero v1.8.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.7.1 github.com/subosito/gotenv v1.2.0 // indirect
github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26 golang.org/x/sys v0.42.0 // indirect
go.etcd.io/bbolt v1.3.5 golang.org/x/text v0.34.0 // indirect
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb gopkg.in/ini.v1 v1.66.2 // indirect
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 gopkg.in/yaml.v2 v2.4.0 // indirect
modernc.org/libc v1.72.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
) )
+406 -341
View File
@@ -3,390 +3,284 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3 h1:AVXDdKsrtX33oR9fbCMu/+c1o8Ofjq6Ku/MInaLVg5Y=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/bigquery v1.0.1 h1:hL+ycaJpVE9M7nLoiXb/Pn10ENE2u+oddxbD8uu0ZVU= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0 h1:Kt+gOPPp2LEPWp8CSfxhsM8ik9CcyE/gYu+0r+RnZvM= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0 h1:9x7Bx0A9R5/M9jibeJeZWqjeVEIxYW9fZYqB9a70/bY= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1 h1:W9tAK3E57P75u0XLLR82LZyw8VpAnhmyTOxW9qzmyj8=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0 h1:VV2nUM3wwLLGh9lSABFgZMjInyUbJeaRSE64WuAIQ+4= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/breml/rootcerts v0.2.1 h1:GZMVDXOs945764NFck0vtHSjktKYubOFM0kjf5HAuwc=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/breml/rootcerts v0.2.1/go.mod h1:24FDtzYMpqIeYC7QzaE8VPRQaFZU5TIUDlyk8qwjD88=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c h1:+0HFd5KSZ/mm3JmhmrDukiId5iR6w4+BdFtfSy4yWIc=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/etcd v3.3.13+incompatible h1:8F3hqu9fGYLBifCmRCJsicFqDx/D68Rt3q1JMazcgBQ= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-systemd/v22 v22.4.0 h1:y9YHcjnjynCd/DVbg5j9L/33jQM3MxJlbj/zWskzfGU=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-systemd/v22 v22.4.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954 h1:RMLoZVzv4GliuWafOuPuQDKSm1SJph7uCRnnS61JAn4= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.1 h1:ALxjCrTf1aflOlkhMnCUP86MubbWFrzB3gkRPReLpTo= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.0 h1:tV1g1XENQ8ku4Bq3K9ub2AtgG+p16SmzeMSGTwrOKdE=
github.com/go-chi/cors v1.1.1 h1:eHuqxsIw89iXcWnWUN8R72JMibABJTN/4IOYI5WERvw= github.com/go-chi/cors v1.2.0/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/cors v1.1.1/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f h1:Jnx61latede7zDD3DiiP4gmNz33uK0U5HDUaF0a/HVQ=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0 h1:bM6ZAFZmc/wPFaRDi0d5L7hGEZEx/2u+Tmr2evNHDiI=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0 h1:BNQPM9ytxj6jbjjdRPioQ94T6YXriSopn0i8COv6SRA=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1 h1:LnuDWGNsoajlhGyHJvuWW6FVqRl8JOTPqS6CPTsYjhY=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/hashicorp/mdns v1.0.0 h1:WhIgCr5a7AaVH6jPUwjtRuuE7/RDufnUvzIr48smyxs=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0 h1:ZqfnKyx9KGpRcW04j5nnPDgRgoXUeLh2YFBeFzphcA0=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY= github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223 h1:F9x/1yl3T2AeKLr2AMdilSD8+f9bvMnNN8VS5iDtovc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc= github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc=
github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68= github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs= github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30 h1:BHT1/DKsYDGkUgQ2jmMaozVcdk+sVfz0+1ZJq4zkWgw=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw= github.com/pires/go-proxyproto v0.6.1 h1:EBupykFmo22SDjv4fQVQd2J9NOoLPmyZA/15ldOGkPw=
github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= github.com/pires/go-proxyproto v0.6.1/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pires/go-proxyproto v0.1.3 h1:2XEuhsQluSNA5QIQkiUv8PfgZ51sNYIQkq/yFquiSQM= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pires/go-proxyproto v0.1.3/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pires/go-proxyproto v0.5.0 h1:A4Jv4ZCaV3AFJeGh5mGwkz4iuWUYMlQ7IoO/GTuSuLo=
github.com/pires/go-proxyproto v0.5.0/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1 h1:VasscCm72135zRysgrJDKsntdmPN+OuU3+nnHYA9wyc=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af h1:gu+uRPtBe88sKxUCEXRoeCvVG90TJmwhiqRpvdhQFng=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f h1:UFr9zpz4xgTnIE5yIMtWAMngCdZ9p/+q6lTbgelo80M=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/spf13/afero v1.8.0 h1:5MmtuhAgYeU6qpa7w7bP0dv6MBYuup0vekhSpSkoq60=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/spf13/afero v1.8.0/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.3.4 h1:8q6vk3hthlpb2SouZcnBVKboxWQWMDNF38bwholZrJc=
github.com/spf13/afero v1.3.4/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/afero v1.5.1 h1:VHu76Lk0LSP1x254maIu2bplkWpfBWI+B+6fdoZprcg=
github.com/spf13/afero v1.5.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk=
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26 h1:UFHFmFfixpmfRBcxuu+LA9l8MdURWVdVNUHxO5n1d2w= github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26 h1:UFHFmFfixpmfRBcxuu+LA9l8MdURWVdVNUHxO5n1d2w=
github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26/go.mod h1:IGhd0qMDsUa9acVjsbsT7bu3ktadtGOHI79+idTew/M= github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26/go.mod h1:IGhd0qMDsUa9acVjsbsT7bu3ktadtGOHI79+idTew/M=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136 h1:A1gGSx58LAGVHUUsOf7IiR0u8Xb6W51gRwfDBhkdcaw=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200801110659-972c09e46d76 h1:U7GPaoQyQmX+CBRWXKrvRzWTbd+slqeSh8uARsIyhAw= golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
golang.org/x/image v0.0.0-20200801110659-972c09e46d76/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0 h1:sfUMP1Gu8qASkorDVjnMuvgJzwFbTZSeXFiGBYAVdl4=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -394,82 +288,184 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200819035508-9a32b3aa38f5 h1:2r6BWB+sWBIRVv2mC6sYNpdbplZte/1k1drwUKUpS60= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200819035508-9a32b3aa38f5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e h1:XNp2Flc/1eWQGk5BLzqTAN7fQIwIbfyVTuVxXxZh73M= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0 h1:Q3Ui3V3/CVinFWFiW39Iw0kMuVrRzYX0wN6OPFp0lTA=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -478,40 +474,109 @@ google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRn
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a h1:Ob5/580gVHBJZgXnff1cZDbG+xLtMVE5mDRTe+nIsX4=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.60.0 h1:P5ZzC7RJO04094NJYlEnBdFK2wwmnCAy/+7sAzvWs60=
gopkg.in/ini.v1 v1.60.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+7 -5
View File
@@ -2,13 +2,15 @@ package main
import ( import (
"flag" "flag"
_ "time/tzdata"
"github.com/librespeed/speedtest-go/config"
"github.com/librespeed/speedtest-go/database"
"github.com/librespeed/speedtest-go/results"
"github.com/librespeed/speedtest-go/web"
_ "github.com/breml/rootcerts"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/librespeed/speedtest/config"
"github.com/librespeed/speedtest/database"
"github.com/librespeed/speedtest/results"
"github.com/librespeed/speedtest/web"
) )
var ( var (
+119
View File
@@ -0,0 +1,119 @@
package results
import (
"crypto/rand"
"encoding/base64"
"encoding/binary"
"fmt"
"os"
"path/filepath"
"sync"
"github.com/oklog/ulid/v2"
log "github.com/sirupsen/logrus"
)
// ID obfuscation provides an optional privacy layer for test result URLs.
// When enabled, the telemetry endpoint returns an obfuscated ULID that must
// be deobfuscated before looking up the result.
//
// This is NOT cryptographically secure — it prevents casual ID guessing,
// matching the behavior of the PHP version's idObfuscation.php.
var (
obfuscationSalt uint32
obfuscationSaltOnce sync.Once
obfuscationSaltErr error
)
const obfuscationSaltFile = "idObfuscation_salt.bin"
func getOrCreateObfuscationSalt() (uint32, error) {
obfuscationSaltOnce.Do(func() {
data, err := os.ReadFile(obfuscationSaltFile)
if err == nil && len(data) == 4 {
obfuscationSalt = binary.LittleEndian.Uint32(data)
return
}
saltBytes := make([]byte, 4)
if _, err := rand.Read(saltBytes); err != nil {
obfuscationSaltErr = fmt.Errorf("failed to generate obfuscation salt: %w", err)
return
}
obfuscationSalt = binary.LittleEndian.Uint32(saltBytes)
dir := filepath.Dir(obfuscationSaltFile)
if dir != "." {
if err := os.MkdirAll(dir, 0755); err != nil {
log.Warnf("Could not create directory for obfuscation salt file: %s", err)
}
}
if err := os.WriteFile(obfuscationSaltFile, saltBytes, 0644); err != nil {
log.Warnf("Could not save obfuscation salt file: %s", err)
}
})
return obfuscationSalt, obfuscationSaltErr
}
// obfuscateBytes applies reversible transform on ULID bytes:
// XOR the first 4 bytes with the salt (simple but effective for casual privacy)
func obfuscateBytes(data []byte) []byte {
salt, err := getOrCreateObfuscationSalt()
if err != nil || len(data) < 4 {
return data
}
result := make([]byte, len(data))
copy(result, data)
val := binary.LittleEndian.Uint32(result[:4])
val ^= salt
binary.LittleEndian.PutUint32(result[:4], val)
return result
}
// deobfuscateBytes reverses obfuscateBytes (XOR is self-inverse)
var deobfuscateBytes = obfuscateBytes
// ObfuscateULID transforms a ULID string to its obfuscated (base64) form
func ObfuscateULID(id string) string {
parsed, err := ulid.Parse(id)
if err != nil {
return id
}
obfuscated := obfuscateBytes(parsed[:])
return base64.RawURLEncoding.EncodeToString(obfuscated)
}
// DeobfuscateULID reverses ULID obfuscation
func DeobfuscateULID(obfuscated string) (string, error) {
data, err := base64.RawURLEncoding.DecodeString(obfuscated)
if err != nil {
return "", fmt.Errorf("invalid obfuscated ID encoding: %w", err)
}
if len(data) != 16 {
return "", fmt.Errorf("invalid obfuscated ID length: %d", len(data))
}
deobfuscated := deobfuscateBytes(data)
var id ulid.ULID
copy(id[:], deobfuscated)
return id.String(), nil
}
// ResolveID takes an ID string and returns the database ULID.
// It tries the raw input first, then attempts deobfuscation.
func ResolveID(id string) string {
// First try: use as-is (plain ULID)
if _, err := ulid.Parse(id); err == nil {
return id
}
// Second try: deobfuscate
if deobfuscated, err := DeobfuscateULID(id); err == nil {
return deobfuscated
}
return id
}
+102
View File
@@ -0,0 +1,102 @@
package results
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/go-chi/render"
"github.com/librespeed/speedtest-go/config"
"github.com/librespeed/speedtest-go/database"
log "github.com/sirupsen/logrus"
)
// formatValue formats a numeric string for display, matching PHP behavior:
// - values < 10: 2 decimal places
// - values < 100: 1 decimal place
// - values >= 100: 0 decimal places
func formatValue(d string) string {
val, err := strconv.ParseFloat(d, 64)
if err != nil {
return d
}
if val < 10 {
return strconv.FormatFloat(val, 'f', 2, 64)
}
if val < 100 {
return strconv.FormatFloat(val, 'f', 1, 64)
}
return strconv.FormatFloat(val, 'f', 0, 64)
}
// extractISPName extracts the ISP name from the processedString format:
// "IP - ISP, Country (distance)" → "ISP"
func extractISPName(processedString string) string {
dash := strings.Index(processedString, "-")
if dash == -1 {
return ""
}
isp := strings.TrimSpace(processedString[dash+1:])
par := strings.LastIndex(isp, "(")
if par != -1 {
isp = strings.TrimSpace(isp[:par])
}
return isp
}
// JSONResponse is the structure returned by the JSON results endpoint
type JSONResponse struct {
Timestamp string `json:"timestamp"`
Download string `json:"download"`
Upload string `json:"upload"`
Ping string `json:"ping"`
Jitter string `json:"jitter"`
ISPInfo string `json:"ispinfo"`
}
// JSONResult handles GET /results/json?id=X and returns test results as JSON
func JSONResult(w http.ResponseWriter, r *http.Request) {
conf := config.LoadedConfig()
if conf.DatabaseType == "none" {
render.PlainText(w, r, "Telemetry is disabled")
return
}
rawID := r.FormValue("id")
if rawID == "" {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, map[string]string{"error": "missing id parameter"})
return
}
uuid := ResolveID(rawID)
record, err := database.DB.FetchByUUID(uuid)
if err != nil {
log.Errorf("Error querying database for JSON result: %s", err)
w.WriteHeader(http.StatusNotFound)
render.JSON(w, r, map[string]string{"error": "result not found"})
return
}
// Format values for display (matching PHP json.php behavior)
resp := JSONResponse{
Timestamp: record.Timestamp.Format("2006-01-02 15:04:05"),
Download: formatValue(record.Download),
Upload: formatValue(record.Upload),
Ping: formatValue(record.Ping),
Jitter: formatValue(record.Jitter),
}
// Extract ISP name from ISP info JSON
var result Result
if err := json.Unmarshal([]byte(record.ISPInfo), &result); err == nil {
resp.ISPInfo = extractISPName(result.ProcessedString)
}
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0, s-maxage=0")
w.Header().Add("Cache-Control", "post-check=0, pre-check=0")
w.Header().Set("Pragma", "no-cache")
render.JSON(w, r, resp)
}
+55 -19
View File
@@ -4,11 +4,15 @@ import (
"html/template" "html/template"
"net/http" "net/http"
"github.com/go-chi/render"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/crypto/bcrypt"
"github.com/librespeed/speedtest/config" "github.com/gorilla/securecookie"
"github.com/librespeed/speedtest/database" "github.com/gorilla/sessions"
"github.com/librespeed/speedtest/database/schema" "github.com/librespeed/speedtest-go/config"
"github.com/librespeed/speedtest-go/database"
"github.com/librespeed/speedtest-go/database/schema"
) )
type StatsData struct { type StatsData struct {
@@ -17,6 +21,37 @@ type StatsData struct {
Data []schema.TelemetryData 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) { func Stats(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
t, err := template.New("template").Parse(htmlTemplate) t, err := template.New("template").Parse(htmlTemplate)
@@ -26,7 +61,11 @@ func Stats(w http.ResponseWriter, r *http.Request) {
return return
} }
conf := config.LoadedConfig() if conf.DatabaseType == "none" {
render.PlainText(w, r, "Statistics are disabled")
return
}
var data StatsData var data StatsData
if conf.StatsPassword == "PASSWORD" { if conf.StatsPassword == "PASSWORD" {
@@ -35,16 +74,15 @@ func Stats(w http.ResponseWriter, r *http.Request) {
if !data.NoPassword { if !data.NoPassword {
op := r.FormValue("op") 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" { if op == "logout" {
cookie := &http.Cookie{ session.Values["authenticated"] = false
Name: "logged", session.Options.MaxAge = -1
Value: "false", session.Save(r, w)
} http.Redirect(w, r, conf.BaseURL+"/stats", http.StatusTemporaryRedirect)
http.SetCookie(w, cookie)
http.Redirect(w, r, "/stats", http.StatusTemporaryRedirect)
} else { } else {
data.LoggedIn = true data.LoggedIn = true
@@ -71,14 +109,12 @@ func Stats(w http.ResponseWriter, r *http.Request) {
} }
} else { } else {
if op == "login" { if op == "login" {
session, _ := store.Get(r, "logged")
password := r.FormValue("password") password := r.FormValue("password")
if password == conf.StatsPassword { if checkPassword(password) {
cookie := &http.Cookie{ session.Values["authenticated"] = true
Name: "logged", session.Save(r, w)
Value: "true", http.Redirect(w, r, conf.BaseURL+"/stats", http.StatusTemporaryRedirect)
}
http.SetCookie(w, cookie)
http.Redirect(w, r, "/stats", http.StatusTemporaryRedirect)
} else { } else {
w.WriteHeader(http.StatusForbidden) w.WriteHeader(http.StatusForbidden)
} }
+48 -31
View File
@@ -1,23 +1,23 @@
package results package results
import ( import (
_ "embed"
"encoding/json" "encoding/json"
"image" "image"
"image/color" "image/color"
"image/draw" "image/draw"
"image/png" "image/png"
"io/ioutil"
"math/rand" "math/rand"
"net" "net"
"net/http" "net/http"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"github.com/librespeed/speedtest/config" "github.com/go-chi/render"
"github.com/librespeed/speedtest/database" "github.com/librespeed/speedtest-go/config"
"github.com/librespeed/speedtest/database/schema" "github.com/librespeed/speedtest-go/database"
"github.com/librespeed/speedtest-go/database/schema"
"github.com/golang/freetype" "github.com/golang/freetype"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
@@ -37,8 +37,14 @@ const (
labelUpload = "Upload" labelUpload = "Upload"
) )
//go:embed fonts/NotoSansDisplay-Medium.ttf
var fontMediumBytes []byte
//go:embed fonts/NotoSansDisplay-Light.ttf
var fontLightBytes []byte
var ( 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]?)`) 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]))`) 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]))`)
hostnameRegex = regexp.MustCompile(`"hostname":"([^\\\\"]|\\\\")*"`) hostnameRegex = regexp.MustCompile(`"hostname":"([^\\\\"]|\\\\")*"`)
@@ -81,27 +87,20 @@ type IPInfoResponse struct {
} }
func Initialize(c *config.Config) { func Initialize(c *config.Config) {
statsInitialize(c)
// changed to use Noto Sans instead of OpenSans, due to issue: // changed to use Noto Sans instead of OpenSans, due to issue:
// https://github.com/golang/freetype/issues/8 // https://github.com/golang/freetype/issues/8
if b, err := ioutil.ReadFile(filepath.Join(c.AssetsPath, "NotoSansDisplay-Light.ttf")); err != nil { fLight, err := freetype.ParseFont(fontLightBytes)
log.Fatalf("Error opening NotoSansDisplay-Light font: %s", err) if err != nil {
} else { log.Fatalf("Error parsing NotoSansDisplay-Light font: %s", err)
f, err := freetype.ParseFont(b)
if err != nil {
log.Fatalf("Error parsing NotoSansDisplay-Light font: %s", err)
}
fontLight = f
} }
fontLight = fLight
if b, err := ioutil.ReadFile(filepath.Join(c.AssetsPath, "NotoSansDisplay-Medium.ttf")); err != nil { fMedium, err := freetype.ParseFont(fontMediumBytes)
log.Fatalf("Error opening NotoSansDisplay-Medium font: %s", err) if err != nil {
} else { log.Fatalf("Error parsing NotoSansDisplay-Medium font: %s", err)
f, err := freetype.ParseFont(b)
if err != nil {
log.Fatalf("Error parsing NotoSansDisplay-Medium font: %s", err)
}
fontBold = f
} }
fontBold = fMedium
pingJitterLabelFace = truetype.NewFace(fontBold, &truetype.Options{ pingJitterLabelFace = truetype.NewFace(fontBold, &truetype.Options{
Size: 12, Size: 12,
@@ -147,6 +146,12 @@ func Initialize(c *config.Config) {
} }
func Record(w http.ResponseWriter, r *http.Request) { 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) ipAddr, _, _ := net.SplitHostPort(r.RemoteAddr)
userAgent := r.UserAgent() userAgent := r.UserAgent()
language := r.Header.Get("Accept-Language") language := r.Header.Get("Accept-Language")
@@ -160,13 +165,13 @@ func Record(w http.ResponseWriter, r *http.Request) {
extra := r.FormValue("extra") extra := r.FormValue("extra")
if config.LoadedConfig().RedactIP { if config.LoadedConfig().RedactIP {
ipAddr = "0.0.0.0" ipAddr = "0.0.0.0"
ipv4Regex.ReplaceAllString(ispInfo, "0.0.0.0") ispInfo = ipv4Regex.ReplaceAllString(ispInfo, "0.0.0.0")
ipv4Regex.ReplaceAllString(logs, "0.0.0.0") logs = ipv4Regex.ReplaceAllString(logs, "0.0.0.0")
ipv6Regex.ReplaceAllString(ispInfo, "0.0.0.0") ispInfo = ipv6Regex.ReplaceAllString(ispInfo, "::")
ipv6Regex.ReplaceAllString(logs, "0.0.0.0") logs = ipv6Regex.ReplaceAllString(logs, "::")
hostnameRegex.ReplaceAllString(ispInfo, `"hostname":"REDACTED"`) ispInfo = hostnameRegex.ReplaceAllString(ispInfo, `"hostname":"REDACTED"`)
hostnameRegex.ReplaceAllString(logs, `"hostname":"REDACTED"`) logs = hostnameRegex.ReplaceAllString(logs, `"hostname":"REDACTED"`)
} }
var record schema.TelemetryData var record schema.TelemetryData
@@ -197,14 +202,26 @@ func Record(w http.ResponseWriter, r *http.Request) {
return return
} }
if _, err := w.Write([]byte("id " + uuid.String())); err != nil { responseID := uuid.String()
if config.LoadedConfig().EnableIDObfuscation {
responseID = ObfuscateULID(uuid.String())
}
if _, err := w.Write([]byte("id " + responseID)); err != nil {
log.Errorf("Error writing ID to telemetry request: %s", err) log.Errorf("Error writing ID to telemetry request: %s", err)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
} }
func DrawPNG(w http.ResponseWriter, r *http.Request) { func DrawPNG(w http.ResponseWriter, r *http.Request) {
uuid := r.FormValue("id") conf := config.LoadedConfig()
if conf.DatabaseType == "none" {
return
}
rawID := r.FormValue("id")
uuid := ResolveID(rawID)
record, err := database.DB.FetchByUUID(uuid) record, err := database.DB.FetchByUUID(uuid)
if err != nil { if err != nil {
log.Errorf("Error querying database: %s", err) log.Errorf("Error querying database: %s", err)
+121
View File
@@ -0,0 +1,121 @@
package results
import (
"testing"
)
func TestIPRedaction(t *testing.T) {
tests := []struct {
name string
input string
wantOut string
regex func(string, string) string
redactTo string
}{
{
name: "IPv4 in ispInfo is redacted",
input: `{"ip":"203.0.113.42","org":"AS12345 Example ISP"}`,
wantOut: `{"ip":"0.0.0.0","org":"AS12345 Example ISP"}`,
regex: ipv4Regex.ReplaceAllString,
redactTo: "0.0.0.0",
},
{
name: "multiple IPv4 addresses in logs are all redacted",
input: `connected from 203.0.113.42, forwarded for 198.51.100.7`,
wantOut: `connected from 0.0.0.0, forwarded for 0.0.0.0`,
regex: ipv4Regex.ReplaceAllString,
redactTo: "0.0.0.0",
},
{
name: "empty string is handled safely by IPv4 regex",
input: "",
wantOut: "",
regex: ipv4Regex.ReplaceAllString,
redactTo: "0.0.0.0",
},
{
name: "IPv6 in ispInfo is redacted",
input: `{"ip":"2001:0db8:85a3:0000:0000:8a2e:0370:7334","org":"AS12345 Example ISP"}`,
wantOut: `{"ip":"::","org":"AS12345 Example ISP"}`,
regex: ipv6Regex.ReplaceAllString,
redactTo: "::",
},
{
name: "empty string is handled safely by IPv6 regex",
input: "",
wantOut: "",
regex: ipv6Regex.ReplaceAllString,
redactTo: "::",
},
{
name: "hostname in ispInfo is redacted",
input: `{"ip":"0.0.0.0","hostname":"client.example.com","org":"AS12345 Example ISP"}`,
wantOut: `{"ip":"0.0.0.0","hostname":"REDACTED","org":"AS12345 Example ISP"}`,
regex: hostnameRegex.ReplaceAllString,
redactTo: `"hostname":"REDACTED"`,
},
{
name: "empty string is handled safely by hostname regex",
input: "",
wantOut: "",
regex: hostnameRegex.ReplaceAllString,
redactTo: `"hostname":"REDACTED"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.regex(tt.input, tt.redactTo)
if got != tt.wantOut {
t.Errorf("\ngot: %s\nwant: %s", got, tt.wantOut)
}
})
}
}
func TestIPRedactionChain(t *testing.T) {
tests := []struct {
name string
ispInfo string
logs string
wantISP string
wantLogs string
}{
{
name: "IPv4 client: addresses and hostname redacted",
ispInfo: `{"ip":"203.0.113.42","hostname":"client.example.com","org":"AS12345 Example ISP"}`,
logs: `connected from 203.0.113.42 and 198.51.100.7`,
wantISP: `{"ip":"0.0.0.0","hostname":"REDACTED","org":"AS12345 Example ISP"}`,
wantLogs: `connected from 0.0.0.0 and 0.0.0.0`,
},
{
name: "IPv6 client: redacted as :: to preserve address family for debugging",
ispInfo: `{"ip":"2001:0db8:85a3:0000:0000:8a2e:0370:7334","hostname":"client.example.com","org":"AS12345 Example ISP"}`,
logs: `connected from 2001:0db8:85a3:0000:0000:8a2e:0370:7334`,
wantISP: `{"ip":"::","hostname":"REDACTED","org":"AS12345 Example ISP"}`,
wantLogs: `connected from ::`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ispInfo := tt.ispInfo
logs := tt.logs
// Mirror the full redaction block in Record()
ispInfo = ipv4Regex.ReplaceAllString(ispInfo, "0.0.0.0")
logs = ipv4Regex.ReplaceAllString(logs, "0.0.0.0")
ispInfo = ipv6Regex.ReplaceAllString(ispInfo, "::")
logs = ipv6Regex.ReplaceAllString(logs, "::")
ispInfo = hostnameRegex.ReplaceAllString(ispInfo, `"hostname":"REDACTED"`)
logs = hostnameRegex.ReplaceAllString(logs, `"hostname":"REDACTED"`)
if ispInfo != tt.wantISP {
t.Errorf("ispInfo\ngot: %s\nwant: %s", ispInfo, tt.wantISP)
}
if logs != tt.wantLogs {
t.Errorf("logs\ngot: %s\nwant: %s", logs, tt.wantLogs)
}
})
}
}
+14
View 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
View 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
View 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
View 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
View 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
+24 -5
View File
@@ -2,28 +2,47 @@
bind_address="" bind_address=""
# backend listen port # backend listen port
listen_port=8989 listen_port=8989
# change the base URL
# url_base="/librespeed"
# proxy protocol port, use 0 to disable # proxy protocol port, use 0 to disable
proxyprotocol_port=0 proxyprotocol_port=0
# Server location # Server location
server_lat=0 server_lat=1
server_lng=0 server_lng=1
# ipinfo.io API key, if applicable # ipinfo.io API key, if applicable
ipinfo_api_key="" ipinfo_api_key=""
# assets directory path, defaults to `assets` in the same directory # assets directory path, defaults to `assets` in the same directory
assets_path="./assets" assets_path=""
# password for logging into statistics page # password for logging into statistics page
statistics_password="PASSWORD" statistics_password="PASSWORD"
# redact IP addresses # redact IP addresses
redact_ip_addresses=false 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, sqlite, mssql
database_type="bolt" # if none is specified, no telemetry/stats will be recorded, and no result PNG will be generated
database_type="memory"
database_hostname="" database_hostname=""
database_name="" database_name=""
database_username="" database_username=""
database_password="" database_password=""
# database port (optional, defaults to driver default)
database_port=""
# if you use `bolt` as database, set database_file to database file location # if you use `bolt` as database, set database_file to database file location
# if you use `sqlite` as database, set database_file to database file location
database_file="speedtest.db" database_file="speedtest.db"
# GeoIP offline database (.mmdb format) for ISP and country detection when ipinfo.io API is not available
# This is used as a fallback, matching the PHP backend behavior.
# Leave empty to disable.
# geoip_database_file="country_asn.mmdb"
# 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
View 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
View 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
View 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
View 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
+45
View File
@@ -0,0 +1,45 @@
/**
* Feature switch for enabling the new LibreSpeed design
*
* This script checks for:
* 1. URL parameter: ?design=new or ?design=old
* 2. Default behavior: Shows the classic design
*
* Note: This script is only loaded on the root index.html
*/
(function () {
'use strict';
// Don't run this script if we're already on a specific design page
const currentPath = window.location.pathname;
if (currentPath.includes('index-classic.html') || currentPath.includes('index-modern.html')) {
return;
}
// Check URL parameters first
const urlParams = new URLSearchParams(window.location.search);
const designParam = urlParams.get('design');
if (designParam === 'new') {
redirectToNewDesign();
return;
}
if (designParam === 'old' || designParam === 'classic') {
redirectToOldDesign();
return;
}
// Default to classic design
redirectToOldDesign();
function redirectToNewDesign() {
const currentParams = window.location.search;
window.location.href = 'index-modern.html' + currentParams;
}
function redirectToOldDesign() {
const currentParams = window.location.search;
window.location.href = 'index-classic.html' + currentParams;
}
})();
@@ -10,21 +10,21 @@ function I(i){return document.getElementById(i);}
//LIST OF TEST SERVERS. See documentation for details if needed //LIST OF TEST SERVERS. See documentation for details if needed
var SPEEDTEST_SERVERS=[ var SPEEDTEST_SERVERS=[
{ //this is my demo server, remove it { //this server doesn't actually exist, remove it
name:"Speedtest Demo Server (Helsinki)", //user friendly name for the server name:"Example Server 1", //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 server:"//test1.mydomain.com/", //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) dlURL:"backend/garbage.php", //path to download test on this server (garbage.php or replacement)
ulURL:"empty", //path to upload test on this server (empty.php or replacement) ulURL:"backend/empty.php", //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) pingURL:"backend/empty.php", //path to ping/jitter test on this server (empty.php or replacement)
getIpURL:"getIP" //path to getIP on this server (getIP.php or replacement) getIpURL:"backend/getIP.php" //path to getIP on this server (getIP.php or replacement)
}, },
{ //this is my demo server, remove it { //this server doesn't actually exist, remove it
name:"Old Speedtest Demo Server", name:"Example Server 2", //user friendly name for the server
server:"//mpotdemo.fdossena.com/", server:"//test2.example.com/", //URL to the server. // at the beginning will be replaced with http:// or https:// automatically
dlURL:"garbage", dlURL:"garbage.php", //path to download test on this server (garbage.php or replacement)
ulURL:"empty", ulURL:"empty.php", //path to upload test on this server (empty.php or replacement)
pingURL:"empty", pingURL:"empty.php", //path to ping/jitter test on this server (empty.php or replacement)
getIpURL:"getIP" getIpURL:"getIP.php" //path to getIP on this server (getIP.php or replacement)
} }
//add other servers here, comma separated //add other servers here, comma separated
]; ];
@@ -32,29 +32,48 @@ var SPEEDTEST_SERVERS=[
//INITIALIZE SPEEDTEST //INITIALIZE SPEEDTEST
var s=new Speedtest(); //create speedtest object var s=new Speedtest(); //create speedtest object
s.setParameter("telemetry_level","basic"); //enable telemetry s.setParameter("telemetry_level","basic"); //enable telemetry
s.addTestPoints(SPEEDTEST_SERVERS); //add list of servers
//SERVER AUTO SELECTION //SERVER AUTO SELECTION
function initServers(){ function initServers(){
s.selectServer(function(server){ var noServersAvailable=function(){
if(server!=null){ //at least 1 server is available I("message").innerHTML="No servers available";
I("loading").className="hidden"; //hide loading message }
//populate server list for manual selection var runServerSelect=function(){
for(var i=0;i<SPEEDTEST_SERVERS.length;i++){ s.selectServer(function(server){
if(SPEEDTEST_SERVERS[i].pingT==-1) continue; if(server!=null){ //at least 1 server is available
var option=document.createElement("option"); I("loading").className="hidden"; //hide loading message
option.value=i; //populate server list for manual selection
option.textContent=SPEEDTEST_SERVERS[i].name; for(var i=0;i<SPEEDTEST_SERVERS.length;i++){
if(SPEEDTEST_SERVERS[i]===server) option.selected=true; if(SPEEDTEST_SERVERS[i].pingT==-1) continue;
I("server").appendChild(option); var option=document.createElement("option");
} option.value=i;
//show test UI option.textContent=SPEEDTEST_SERVERS[i].name;
I("testWrapper").className="visible"; if(SPEEDTEST_SERVERS[i]===server) option.selected=true;
initUI(); I("server").appendChild(option);
}else{ //no servers are available, the test cannot proceed }
I("message").innerHTML="No servers available"; //show test UI
} I("testWrapper").className="visible";
}); initUI();
}else{ //no servers are available, the test cannot proceed
noServersAvailable();
}
});
}
if(typeof SPEEDTEST_SERVERS === "string"){
//need to fetch list of servers from specified URL
s.loadServerList(SPEEDTEST_SERVERS,function(servers){
if(servers==null){ //failed to load server list
noServersAvailable();
}else{ //server list loaded
SPEEDTEST_SERVERS=servers;
runServerSelect();
}
});
}else{
//hardcoded server list
s.addTestPoints(SPEEDTEST_SERVERS);
runServerSelect();
}
} }
var meterBk=/Trident.*rv:(\d+\.\d+)/i.test(navigator.userAgent)?"#EAEAEA":"#80808040"; var meterBk=/Trident.*rv:(\d+\.\d+)/i.test(navigator.userAgent)?"#EAEAEA":"#80808040";
@@ -333,8 +352,15 @@ function initUI(){
text-align:center; text-align:center;
font-size:0.8em; font-size:0.8em;
color:#808080; 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){ @media all and (max-width:40em){
body{ body{
font-size:0.8em; font-size:0.8em;
@@ -375,7 +401,7 @@ function initUI(){
<p id="message"><span class="loadCircle"></span>Selecting a server...</p> <p id="message"><span class="loadCircle"></span>Selecting a server...</p>
</div> </div>
<div id="testWrapper" class="hidden"> <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> <a class="privacy" href="#" onclick="I('privacyPolicy').style.display=''">Privacy</a>
<div id="serverArea"> <div id="serverArea">
Server: <select id="server" onchange="s.setSelectedServer(SPEEDTEST_SERVERS[this.value])"></select> Server: <select id="server" onchange="s.setSelectedServer(SPEEDTEST_SERVERS[this.value])"></select>
@@ -398,13 +424,13 @@ function initUI(){
<div class="testName">Download</div> <div class="testName">Download</div>
<canvas id="dlMeter" class="meter"></canvas> <canvas id="dlMeter" class="meter"></canvas>
<div id="dlText" class="meterText"></div> <div id="dlText" class="meterText"></div>
<div class="unit">Mbit/s</div> <div class="unit">Mbps</div>
</div> </div>
<div class="testArea"> <div class="testArea">
<div class="testName">Upload</div> <div class="testName">Upload</div>
<canvas id="ulMeter" class="meter"></canvas> <canvas id="ulMeter" class="meter"></canvas>
<div id="ulText" class="meterText"></div> <div id="ulText" class="meterText"></div>
<div class="unit">Mbit/s</div> <div class="unit">Mbps</div>
</div> </div>
</div> </div>
<div id="ipArea"> <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>. Contact this email address for all deletion requests: <a href="mailto:PUT@YOUR_EMAIL.HERE">TO BE FILLED BY DEVELOPER</a>.
</p> </p>
<br/><br/> <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> </div>
</body> </body>
</html> </html>
@@ -10,21 +10,21 @@
//LIST OF TEST SERVERS. See documentation for details if needed //LIST OF TEST SERVERS. See documentation for details if needed
var SPEEDTEST_SERVERS=[ var SPEEDTEST_SERVERS=[
{ //this is my demo server, remove it { //this server doesn't actually exist, remove it
name:"Speedtest Demo Server (Helsinki)", //user friendly name for the server name:"Example Server 1", //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 server:"//test1.mydomain.com/", //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) dlURL:"backend/garbage.php", //path to download test on this server (garbage.php or replacement)
ulURL:"empty", //path to upload test on this server (empty.php or replacement) ulURL:"backend/empty.php", //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) pingURL:"backend/empty.php", //path to ping/jitter test on this server (empty.php or replacement)
getIpURL:"getIP" //path to getIP on this server (getIP.php or replacement) getIpURL:"backend/getIP.php" //path to getIP on this server (getIP.php or replacement)
}, },
{ //this is my demo server, remove it { //this server doesn't actually exist, remove it
name:"Old Speedtest Demo Server", name:"Example Server 2", //user friendly name for the server
server:"//mpotdemo.fdossena.com/", server:"//test2.example.com/", //URL to the server. // at the beginning will be replaced with http:// or https:// automatically
dlURL:"garbage", dlURL:"garbage.php", //path to download test on this server (garbage.php or replacement)
ulURL:"empty", ulURL:"empty.php", //path to upload test on this server (empty.php or replacement)
pingURL:"empty", pingURL:"empty.php", //path to ping/jitter test on this server (empty.php or replacement)
getIpURL:"getIP" getIpURL:"getIP.php" //path to getIP on this server (getIP.php or replacement)
} }
//add other servers here, comma separated //add other servers here, comma separated
]; ];
@@ -32,7 +32,7 @@ var SPEEDTEST_SERVERS=[
//INITIALIZE SPEEDTEST //INITIALIZE SPEEDTEST
var s=new Speedtest(); //create speedtest object 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 s.onupdate=function(data){ //callback to update data in UI
I("ip").textContent=data.clientIp; I("ip").textContent=data.clientIp;
I("dlText").textContent=(data.testState==1&&data.dlStatus==0)?"...":data.dlStatus; 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(); initUI();
} }
} }
function selectServer(){ //called when the page is fully loaded function selectServer(){ //called after loading server list
I("startStopBtn").style.display="none"; //hide start/stop button during server selection
s.selectServer(function(server){ //run server selection. When the server has been selected, display it in the UI 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 if(server==null){
I("serverId").textContent=server.name; //show name of test server 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 function startStop(){ //start/stop button pressed
@@ -192,12 +212,12 @@ function I(id){return document.getElementById(id);}
<div class="testArea"> <div class="testArea">
<div class="testName">Download</div> <div class="testName">Download</div>
<div id="dlText" class="meterText"></div> <div id="dlText" class="meterText"></div>
<div class="unit">Mbit/s</div> <div class="unit">Mbps</div>
</div> </div>
<div class="testArea"> <div class="testArea">
<div class="testName">Upload</div> <div class="testName">Upload</div>
<div id="ulText" class="meterText"></div> <div id="ulText" class="meterText"></div>
<div class="unit">Mbit/s</div> <div class="unit">Mbps</div>
</div> </div>
</div> </div>
<div class="testGroup"> <div class="testGroup">
@@ -219,7 +239,7 @@ function I(id){return document.getElementById(id);}
<a href="https://github.com/librespeed/speedtest">Source code</a> <a href="https://github.com/librespeed/speedtest">Source code</a>
<script type="text/javascript"> <script type="text/javascript">
initUI(); initUI();
selectServer(); loadServers();
</script> </script>
</body> </body>
</html> </html>
@@ -157,12 +157,12 @@ function I(id){return document.getElementById(id);}
<div class="testArea"> <div class="testArea">
<div class="testName">Download</div> <div class="testName">Download</div>
<div id="dlText" class="meterText"></div> <div id="dlText" class="meterText"></div>
<div class="unit">Mbit/s</div> <div class="unit">Mbps</div>
</div> </div>
<div class="testArea"> <div class="testArea">
<div class="testName">Upload</div> <div class="testName">Upload</div>
<div id="ulText" class="meterText"></div> <div id="ulText" class="meterText"></div>
<div class="unit">Mbit/s</div> <div class="unit">Mbps</div>
</div> </div>
</div> </div>
</div> </div>
@@ -242,13 +242,13 @@ function initUI(){
<div class="testName">Download</div> <div class="testName">Download</div>
<canvas id="dlMeter" class="meter"></canvas> <canvas id="dlMeter" class="meter"></canvas>
<div id="dlText" class="meterText"></div> <div id="dlText" class="meterText"></div>
<div class="unit">Mbit/s</div> <div class="unit">Mbps</div>
</div> </div>
<div class="testArea"> <div class="testArea">
<div class="testName">Upload</div> <div class="testName">Upload</div>
<canvas id="ulMeter" class="meter"></canvas> <canvas id="ulMeter" class="meter"></canvas>
<div id="ulText" class="meterText"></div> <div id="ulText" class="meterText"></div>
<div class="unit">Mbit/s</div> <div class="unit">Mbps</div>
</div> </div>
</div> </div>
<div id="ipArea"> <div id="ipArea">
@@ -160,12 +160,12 @@ function I(id){return document.getElementById(id);}
<div class="testArea"> <div class="testArea">
<div class="testName">Download</div> <div class="testName">Download</div>
<div id="dlText" class="meterText"></div> <div id="dlText" class="meterText"></div>
<div class="unit">Mbit/s</div> <div class="unit">Mbps</div>
</div> </div>
<div class="testArea"> <div class="testArea">
<div class="testName">Upload</div> <div class="testName">Upload</div>
<div id="ulText" class="meterText"></div> <div id="ulText" class="meterText"></div>
<div class="unit">Mbit/s</div> <div class="unit">Mbps</div>
</div> </div>
</div> </div>
<div class="testGroup"> <div class="testGroup">
@@ -180,12 +180,12 @@ function I(id){return document.getElementById(id);}
<div class="testArea"> <div class="testArea">
<div class="testName">Download</div> <div class="testName">Download</div>
<div id="dlText" class="meterText"></div> <div id="dlText" class="meterText"></div>
<div class="unit">Mbit/s</div> <div class="unit">Mbps</div>
</div> </div>
<div class="testArea"> <div class="testArea">
<div class="testName">Upload</div> <div class="testName">Upload</div>
<div id="ulText" class="meterText"></div> <div id="ulText" class="meterText"></div>
<div class="unit">Mbit/s</div> <div class="unit">Mbps</div>
</div> </div>
</div> </div>
<div class="testGroup"> <div class="testGroup">
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

+22
View File
@@ -0,0 +1,22 @@
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_373_537)">
<path
d="M26 12L16 22L6 12"
stroke="#625B6B"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_373_537">
<rect width="32" height="32" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 500 B

+14
View File
@@ -0,0 +1,14 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_373_731)">
<path d="M25 7L7 25" stroke="#F5F5F5" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M25 25L7 7" stroke="#F5F5F5" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
</g>
<defs>
<clipPath id="clip0_373_731">
<rect width="32" height="32" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 544 B

+19
View File
@@ -0,0 +1,19 @@
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M16.0345 0.123047H3.20289C1.88703 0.123047 0.820312 1.18976 0.820312 2.50562V15.3373C0.820312 16.6531 1.88703 17.7198 3.20289 17.7198H16.0345C17.3504 17.7198 18.4171 16.6531 18.4171 15.3373V2.50562C18.4171 1.18976 17.3504 0.123047 16.0345 0.123047Z"
fill="#F5F5F5" />
<path
d="M17.1947 2.38135H3.55467L3.42638 9.32426L8.84467 9.49601L8.25888 9.99686L2.04297 15.4616L16.3914 15.3137V8.62233L10.1332 7.52898L17.1947 2.38135Z"
fill="url(#paint0_linear_373_542)" />
<defs>
<linearGradient id="paint0_linear_373_542" x1="2.04297" y1="8.92149" x2="17.1947"
y2="8.92149" gradientUnits="userSpaceOnUse">
<stop stop-color="#D63BC6" />
<stop offset="0.3478" stop-color="#7419B1" />
<stop offset="0.5389" stop-color="#3D06A5" />
<stop offset="0.7532" stop-color="#485DC4" />
<stop offset="1" stop-color="#5CF9FD" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+66
View File
@@ -0,0 +1,66 @@
<svg
width="153"
height="18"
viewBox="0 0 153 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="logo"
>
<path
d="M87.0345 0.123047H74.2029C72.887 0.123047 71.8203 1.18976 71.8203 2.50562V15.3373C71.8203 16.6531 72.887 17.7198 74.2029 17.7198H87.0345C88.3504 17.7198 89.4171 16.6531 89.4171 15.3373V2.50562C89.4171 1.18976 88.3504 0.123047 87.0345 0.123047Z"
fill="#F5F5F5"
/>
<path
d="M88.1947 2.38135H74.5547L74.4264 9.32426L79.8447 9.49601L79.2589 9.99686L73.043 15.4616L87.3914 15.3137V8.62233L81.1332 7.52898L88.1947 2.38135Z"
fill="url(#paint0_linear_373_540)"
/>
<path
d="M0 17.4134V0H3.68171V14.378H11.1473V17.4134H0Z"
fill="#F5F5F5"
/>
<path d="M17.2686 0V17.4134H13.5869V0H17.2686Z" fill="#F5F5F5" />
<path
d="M20.3038 17.4134V0H27.2761C28.5572 0 29.6258 0.189989 30.4818 0.569601C31.3374 0.94958 31.9808 1.47506 32.4117 2.14682C32.8425 2.81857 33.058 3.59104 33.058 4.46387C33.058 5.14385 32.9218 5.74068 32.6497 6.25365C32.3776 6.76661 32.0048 7.18598 31.5317 7.51218C31.0582 7.83802 30.5183 8.06886 29.9118 8.20508V8.37499C30.5749 8.40366 31.1972 8.59042 31.7783 8.93633C32.3593 9.28225 32.8311 9.76544 33.1938 10.3859C33.5566 11.0068 33.7379 11.7452 33.7379 12.6008C33.7379 13.5249 33.51 14.3483 33.0536 15.0709C32.5973 15.7936 31.9242 16.365 31.0341 16.7844C30.144 17.2038 29.0475 17.4135 27.7438 17.4135L20.3038 17.4134ZM23.9855 7.28666H26.7148C27.2195 7.28666 27.6686 7.19739 28.0626 7.01889C28.4565 6.84039 28.7684 6.58661 28.9978 6.25791C29.2276 5.92921 29.3423 5.53524 29.3423 5.07609C29.3423 4.44666 29.1197 3.93942 28.6748 3.55408C28.2296 3.16874 27.5991 2.97589 26.7829 2.97589H23.9855V7.28666ZM23.9855 14.4034H26.9869C28.0127 14.4034 28.7612 14.2067 29.2319 13.8123C29.7021 13.4184 29.9373 12.8929 29.9373 12.2351C29.9373 11.7534 29.8212 11.3282 29.5889 10.9597C29.3563 10.5913 29.0261 10.3023 28.5981 10.0926C28.1701 9.88294 27.6615 9.77792 27.0722 9.77792H23.9855L23.9855 14.4034Z"
fill="#F5F5F5"
/>
<path
d="M36.127 17.4134V0H42.9971C44.3123 0 45.436 0.233706 46.3684 0.701485C47.3011 1.1689 48.0123 1.82953 48.5027 2.68232C48.993 3.5358 49.2382 4.53769 49.2382 5.68834C49.2382 6.84472 48.9887 7.83798 48.4901 8.66853C47.9911 9.4987 47.2699 10.135 46.3261 10.5773C45.3823 11.0193 44.2413 11.2404 42.9036 11.2404H38.3039V8.28139H42.3086C43.0115 8.28139 43.5955 8.18533 44.06 7.99248C44.5249 7.79963 44.8723 7.51071 45.1017 7.12501C45.3315 6.73967 45.4462 6.26077 45.4462 5.68831C45.4462 5.11011 45.3315 4.62262 45.1017 4.2258C44.8723 3.82897 44.5235 3.52717 44.0557 3.31997C43.5883 3.11313 43.0001 3.00992 42.2917 3.00992H39.8087V17.4134L36.127 17.4134ZM45.531 9.48901L49.859 17.4134H45.7945L41.5604 9.48901H45.531Z"
fill="#F5F5F5"
/>
<path
d="M51.8398 17.4134V0H63.5735V3.03539H55.5216V7.18451H62.9699V10.2203H55.5216V14.378H63.6076V17.4134L51.8398 17.4134Z"
fill="#F5F5F5"
/>
<path
d="M92.8662 17.4134V0H99.7364C101.058 0 102.182 0.250554 103.112 0.752396C104.042 1.25387 104.752 1.94857 105.242 2.83538C105.733 3.72256 105.978 4.74452 105.978 5.90091C105.978 7.05729 105.728 8.07745 105.229 8.96177C104.73 9.84609 104.009 10.5347 103.066 11.0279C102.122 11.5212 100.981 11.7677 99.6432 11.7677H95.2643V8.8173H99.0481C99.7564 8.8173 100.341 8.694 100.804 8.44737C101.266 8.20075 101.612 7.85773 101.841 7.41862C102.071 6.97914 102.185 6.47337 102.185 5.90091C102.185 5.32272 102.071 4.81658 101.841 4.38319C101.612 3.94948 101.264 3.61215 100.8 3.37129C100.335 3.13042 99.745 3.00995 99.0309 3.00995H96.5482V17.4134H92.8662Z"
fill="#F5F5F5"
/>
<path
d="M108.375 17.4134V0H120.109V3.03539H112.057V7.18451H119.505V10.2203H112.057V14.378H120.143V17.4134L108.375 17.4134Z"
fill="#F5F5F5"
/>
<path
d="M123.042 17.4134V0H134.776V3.03539H126.723V7.18451H134.172V10.2203H126.723V14.378H134.809V17.4134L123.042 17.4134Z"
fill="#F5F5F5"
/>
<path
d="M143.882 17.4134H137.709V0H143.933C145.685 0 147.192 0.34698 148.457 1.04131C149.721 1.736 150.694 2.73213 151.378 4.03012C152.06 5.32844 152.402 6.88165 152.402 8.68967C152.402 10.5035 152.06 12.0624 151.378 13.3661C150.694 14.6698 149.716 15.6702 148.444 16.3674C147.171 17.0647 145.651 17.4134 143.882 17.4134ZM141.391 14.259H143.729C144.817 14.259 145.734 14.0647 146.48 13.6765C147.225 13.2883 147.787 12.6847 148.163 11.8652C148.54 11.0465 148.729 9.98763 148.729 8.68963C148.729 7.40276 148.54 6.35143 148.163 5.53521C147.787 4.71865 147.227 4.11788 146.484 3.73254C145.741 3.34721 144.826 3.15435 143.737 3.15435H141.391L141.391 14.259Z"
fill="#F5F5F5"
/>
<defs>
<linearGradient
id="paint0_linear_373_540"
x1="73.043"
y1="8.92149"
x2="88.1947"
y2="8.92149"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#D63BC6" />
<stop offset="0.3478" stop-color="#7419B1" />
<stop offset="0.5389" stop-color="#3D06A5" />
<stop offset="0.7532" stop-color="#485DC4" />
<stop offset="1" stop-color="#5CF9FD" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

+16 -16
View File
@@ -3,23 +3,13 @@
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no" />
<meta charset="UTF-8" /> <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" src="speedtest.js"></script>
<script type="text/javascript"> <script type="text/javascript">
function I(i){return document.getElementById(i);} function I(i){return document.getElementById(i);}
//INITIALIZE SPEEDTEST //INITIALIZE SPEEDTEST
var s=new Speedtest(); //create speedtest object var s=new Speedtest(); //create speedtest object
s.setParameter("telemetry_level","basic"); //enable telemetry 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 meterBk=/Trident.*rv:(\d+\.\d+)/i.test(navigator.userAgent)?"#EAEAEA":"#80808040";
var dlColor="#6060AA", var dlColor="#6060AA",
@@ -268,8 +258,15 @@ function initUI(){
text-align:center; text-align:center;
font-size:0.8em; font-size:0.8em;
color:#808080; 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){ @media all and (max-width:40em){
body{ body{
font-size:0.8em; font-size:0.8em;
@@ -301,13 +298,13 @@ function initUI(){
<div class="testName">Download</div> <div class="testName">Download</div>
<canvas id="dlMeter" class="meter"></canvas> <canvas id="dlMeter" class="meter"></canvas>
<div id="dlText" class="meterText"></div> <div id="dlText" class="meterText"></div>
<div class="unit">Mbit/s</div> <div class="unit">Mbps</div>
</div> </div>
<div class="testArea"> <div class="testArea">
<div class="testName">Upload</div> <div class="testName">Upload</div>
<canvas id="ulMeter" class="meter"></canvas> <canvas id="ulMeter" class="meter"></canvas>
<div id="ulText" class="meterText"></div> <div id="ulText" class="meterText"></div>
<div class="unit">Mbit/s</div> <div class="unit">Mbps</div>
</div> </div>
</div> </div>
<div id="ipArea"> <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>. Contact this email address for all deletion requests: <a href="mailto:PUT@YOUR_EMAIL.HERE">TO BE FILLED BY DEVELOPER</a>.
</p> </p>
<br/><br/> <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> </div>
<script type="text/javascript">setTimeout(function(){initUI()},100);</script> <script type="text/javascript">setTimeout(function(){initUI()},100);</script>
</body> </body>
+152
View File
@@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description"
content="Free and Open Source Speedtest. Run it right now in your browser, or self-host on a PHP, Golang, Rust or Node server. License: LGPL." />
<link rel="shortcut icon" href="images/favicon.svg" />
<script type="text/javascript" src="speedtest.js"></script>
<script type="text/javascript">
// Set this to a different URL to load the server list from another location.
var SPEEDTEST_SERVERS = "server-list.json";
</script>
<script type="text/javascript" src="javascript/index.js"></script>
<link rel="stylesheet" type="text/css" href="styling/index.css" />
<title>LibreSpeed - Free and Open Source Speedtest</title>
</head>
<body>
<header>
<img src="images/logo.svg" alt="LibreSpeed" />
</header>
<main>
<h1>Free and Open Source Speedtest.</h1>
<p class="tagline">No Flash, No Java, No Websockets, No Bullsh*t</p>
<div class="server-selector">
<div class="chosen">
<div class="chevron">
<img src="images/chevron.svg" alt="select..." />
</div>
<p>current server</p>
<h2 id="selected-server">searching nearest server...</h2>
</div>
<ul class="servers"></ul>
<p class="sponsor" id="sponsor">&nbsp;</p>
</div>
<p id="privacy-warning" class="hidden">
by clicking the start button you agree to our privacy policy<br />
<a href="#" id="choose-privacy">or choose your privacy options</a>
</p>
<button class="disabled" id="start-button"></button>
<div class="gauge-layout">
<div class="ping hidden">
<span class="label">Ping</span>:&nbsp;
<span class="value" id="ping">00</span>ms
</div>
<div class="gauge download" id="download-gauge">
<div class="progress"></div>
<div class="speed"></div>
<h1><span id="download-speed">00</span> Mbps</h1>
<h2>Download</h2>
</div>
<div class="gauge upload" id="upload-gauge">
<div class="progress"></div>
<div class="speed"></div>
<h1><span id="upload-speed">00</span> Mbps</h1>
<h2>Upload</h2>
</div>
<div class="jitter hidden">
<span class="label">Jitter</span>:&nbsp;
<span class="value" id="jitter">00</span>ms
</div>
</div>
<button class="small inverted hidden" id="share-results">
Share results
</button>
</main>
<footer>
<p class="source">
<a href="https://github.com/librespeed/speedtest">source code</a>
</p>
</footer>
<dialog id="share">
<div class="close-dialog">
<img src="images/close-button.svg" alt="Close" />
</div>
<img id="results" src="" alt="Test results in graphical form" />
<button id="copy-link">Copy link</button>
</dialog>
<dialog id="privacy">
<div class="close-dialog">
<img src="images/close-button.svg" alt="Close" />
</div>
<section>
<h1>Privacy Policy</h1>
<p>
This HTML5 speed test server is configured with telemetry enabled.
</p>
<h2>What data we collect</h2>
<p>
At the end of the test, the following data is collected and stored:
</p>
<ul>
<li>Test ID</li>
<li>Time of testing</li>
<li>Test results (download and upload speed, ping and jitter)</li>
<li>IP address</li>
<li>ISP information</li>
<li>Approximate location (inferred from IP address, not GPS)</li>
<li>User agent and browser locale</li>
<li>Test log (contains no personal information)</li>
</ul>
<h2>How we use the data</h2>
<p>Data collected through this service is used to:</p>
<ul>
<li>
Allow sharing of test results (sharable image for forums, etc.)
</li>
<li>
To improve the service offered to you (for instance, to detect
problems on our side)
</li>
</ul>
<p>No personal information is disclosed to third parties.</p>
<h2>Your consent</h2>
<p>
By starting the test, you consent to the terms of this privacy policy.
</p>
<h2>Data removal</h2>
<p>
If you want to have your information deleted, you need to provide
either the ID of the test or your IP address. This is the only way to
identify your data, without this information we won't be able to
comply with your request.
</p>
<p>
Contact this email address for all deletion requests:
<a href="mailto:PUT@YOUR_EMAIL.HERE">TO BE FILLED BY DEVELOPER</a>.
</p>
</section>
<button id="close-privacy">Close</button>
</dialog>
</body>
</html>
+16
View File
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<link rel="shortcut icon" href="favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no" />
<meta charset="UTF-8" />
<script type="text/javascript" src="design-switch.js"></script>
<title>LibreSpeed</title>
</head>
<body>
<p>Loading...</p>
</body>
</html>
+447
View File
@@ -0,0 +1,447 @@
/**
* Design by fromScratch Studio - 2022, 2023 (fromscratch.io)
* Implementation in HTML/CSS/JS by Timendus - 2024 (https://github.com/Timendus)
*
* See https://github.com/librespeed/speedtest/issues/585
*/
// States the UI can be in
const INITIALIZING = 0;
const READY = 1;
const RUNNING = 2;
const FINISHED = 3;
// Keep some global state here
const testState = {
state: INITIALIZING,
speedtest: null,
servers: [],
selectedServerDirty: false,
testData: null,
testDataDirty: false,
telemetryEnabled: false,
};
// Bootstrap the application when the DOM is ready
window.addEventListener("DOMContentLoaded", async () => {
createSpeedtest();
hookUpButtons();
startRenderingLoop();
applySettingsJSON();
applyServerListJSON();
});
/**
* Create a new Speedtest and hook it into the global state
*/
function createSpeedtest() {
testState.speedtest = new Speedtest();
testState.speedtest.onupdate = (data) => {
testState.testData = data;
testState.testDataDirty = true;
};
testState.speedtest.onend = (aborted) =>
(testState.state = aborted ? READY : FINISHED);
}
/**
* Make all the buttons respond to the right clicks
*/
function hookUpButtons() {
document
.querySelector("#start-button")
.addEventListener("click", startButtonClickHandler);
document
.querySelector("#choose-privacy")
.addEventListener("click", () =>
document.querySelector("#privacy").showModal()
);
document
.querySelector("#share-results")
.addEventListener("click", () =>
document.querySelector("#share").showModal()
);
document
.querySelector("#copy-link")
.addEventListener("click", copyLinkButtonClickHandler);
document
.querySelectorAll(".close-dialog, #close-privacy")
.forEach((element) => {
element.addEventListener("click", () =>
document.querySelectorAll("dialog").forEach((modal) => modal.close())
);
});
}
/**
* Event listener for clicks on the main start button
*/
function startButtonClickHandler() {
switch (testState.state) {
case READY:
case FINISHED:
testState.speedtest.start();
testState.state = RUNNING;
return;
case RUNNING:
testState.speedtest.abort();
// testState.state is updated by `onend` handler of speedtest
return;
default:
return;
}
}
/**
* Event listener for clicks on the "Copy link" button in the modal
*/
async function copyLinkButtonClickHandler() {
const link = document.querySelector("img#results").src;
await navigator.clipboard.writeText(link);
const button = document.querySelector("#copy-link");
button.classList.add("active");
button.textContent = "Copied!";
setTimeout(() => {
button.classList.remove("active");
button.textContent = "Copy link";
}, 3000);
}
/**
* Load settings from settings.json on the server and apply them
*/
async function applySettingsJSON() {
try {
const response = await fetch("settings.json");
const settings = await response.json();
if (!settings || typeof settings !== "object") {
return console.error("Settings are empty or malformed");
}
for (let setting in settings) {
testState.speedtest.setParameter(setting, settings[setting]);
if (
setting == "telemetry_level" &&
settings[setting] &&
settings[setting] != "off" &&
settings[setting] != "disabled" &&
settings[setting] != "false"
) {
testState.telemetryEnabled = true;
document.querySelector("#privacy-warning").classList.remove("hidden");
}
}
} catch (error) {
console.error("Failed to fetch settings:", error);
}
}
/**
* Load server list from the configured source and populate the dropdown
*/
async function applyServerListJSON() {
try {
const serverSource =
typeof globalThis.SPEEDTEST_SERVERS !== "undefined"
? globalThis.SPEEDTEST_SERVERS
: "server-list.json";
const servers = Array.isArray(serverSource)
? serverSource
: await fetch(serverSource).then((response) => response.json());
if (!servers || !Array.isArray(servers) || servers.length === 0) {
return console.error("Server list is empty or malformed");
}
testState.servers = servers;
// If there's only one server, just show it. No reachability checks needed.
if (servers.length === 1) {
populateDropdown(servers);
return;
}
// For multiple servers: first run the built-in selection (which pings servers
// and annotates them with pingT). Only then populate the dropdown so that
// dead servers don't appear.
testState.speedtest.addTestPoints(servers);
testState.speedtest.selectServer((bestServer) => {
const aliveServers = testState.servers.filter((s) => {
// Keep servers that responded to ping (pingT !== -1).
if (s.pingT !== -1) return true;
// Also keep protocol-relative servers ("//...") as a defensive fallback.
// LibreSpeed normalizes them to the page protocol before pinging, so they
// are normally treated like any other server and get a real pingT value.
return typeof s.server === "string" && s.server.startsWith("//");
});
// Prefer to show only reachable servers, but if none are reachable,
// fall back to the full list so users can still pick a server manually.
if (aliveServers.length > 0) {
testState.servers = aliveServers;
}
populateDropdown(testState.servers);
if (bestServer) {
selectServer(bestServer);
} else {
alert(
"Can't reach any of the speedtest servers! But you're on this page. Something weird is going on with your network."
);
}
});
} catch (error) {
console.error("Failed to load server list:", error);
}
}
/**
* Add all the servers to the server selection dropdown and make it actually
* work.
* @param {Array} servers - an array of server objects
*/
function populateDropdown(servers) {
const serverSelector = document.querySelector("div.server-selector");
const serverList = serverSelector.querySelector("ul.servers");
// Reset previous state (populateDropdown can be called multiple times)
serverSelector.classList.remove("single-server");
serverSelector.classList.remove("active");
serverList.classList.remove("active");
serverList.innerHTML = "";
// If we have only a single server, just show it
if (servers.length === 1) {
serverSelector.classList.add("single-server");
selectServer(servers[0]);
return;
}
serverSelector.classList.add("active");
// Make the dropdown open and close (hook only once)
if (serverSelector.dataset.hooked !== "1") {
serverSelector.dataset.hooked = "1";
serverSelector.addEventListener("click", () => {
serverList.classList.toggle("active");
});
document.addEventListener("click", (e) => {
if (e.target.closest("div.server-selector") !== serverSelector)
serverList.classList.remove("active");
});
}
// Populate the list to choose from
servers.forEach((server) => {
const item = document.createElement("li");
const link = document.createElement("a");
link.href = "#";
link.innerHTML = `${server.name}${
server.sponsorName ? ` <span>(${server.sponsorName})</span>` : ""
}`;
link.addEventListener("click", () => selectServer(server));
item.appendChild(link);
serverList.appendChild(item);
});
}
/**
* Set the given server as the selected server for the speedtest
* @param {Object} server - a server object
*/
function selectServer(server) {
testState.speedtest.setSelectedServer(server);
testState.selectedServerDirty = true;
testState.state = READY;
}
/**
* Start the requestAnimationFrame UI rendering loop
*/
function startRenderingLoop() {
// Do these queries once to speed up the rendering itself
const serverSelector = document.querySelector("div.server-selector");
const selectedServer = serverSelector.querySelector("#selected-server");
const sponsor = serverSelector.querySelector("#sponsor");
const startButton = document.querySelector("#start-button");
const privacyWarning = document.querySelector("#privacy-warning");
const gauges = document.querySelectorAll("#download-gauge, #upload-gauge");
const downloadProgress = document.querySelector("#download-gauge .progress");
const uploadProgress = document.querySelector("#upload-gauge .progress");
const downloadGauge = document.querySelector("#download-gauge .speed");
const uploadGauge = document.querySelector("#upload-gauge .speed");
const downloadText = document.querySelector("#download-gauge span");
const uploadText = document.querySelector("#upload-gauge span");
const pingAndJitter = document.querySelectorAll(".ping, .jitter");
const ping = document.querySelector("#ping");
const jitter = document.querySelector("#jitter");
const shareResults = document.querySelector("#share-results");
const copyLink = document.querySelector("#copy-link");
const resultsImage = document.querySelector("#results");
const buttonTexts = {
[INITIALIZING]: "Loading...",
[READY]: "Let's start",
[RUNNING]: "Abort",
[FINISHED]: "Restart",
};
// Show copy link button only if navigator.clipboard is available
copyLink.classList.toggle("hidden", !navigator.clipboard);
function renderUI() {
// Make the main button reflect the current state
startButton.textContent = buttonTexts[testState.state];
startButton.classList.toggle("disabled", testState.state === INITIALIZING);
startButton.classList.toggle("active", testState.state === RUNNING);
// Disable the server selector while test is running
serverSelector.classList.toggle("disabled", testState.state === RUNNING);
// Show selected server
if (testState.selectedServerDirty) {
const server = testState.speedtest.getSelectedServer();
selectedServer.textContent = server.name;
if (server.sponsorName) {
if (server.sponsorURL) {
sponsor.innerHTML = `Sponsor: <a href="${server.sponsorURL}">${server.sponsorName}</a>`;
} else {
sponsor.textContent = `Sponsor: ${server.sponsorName}`;
}
} else {
sponsor.innerHTML = "&nbsp;";
}
testState.selectedServerDirty = false;
}
// Activate the gauges when test running or finished
gauges.forEach((e) =>
e.classList.toggle(
"enabled",
testState.state === RUNNING || testState.state === FINISHED
)
);
// Show ping and jitter if data is available
pingAndJitter.forEach((e) =>
e.classList.toggle(
"hidden",
!(
testState.testData &&
testState.testData.pingStatus &&
testState.testData.jitterStatus
)
)
);
// Show share button after test if server supports it
shareResults.classList.toggle(
"hidden",
!(
testState.state === FINISHED &&
testState.telemetryEnabled &&
testState.testData.testId
)
);
if (testState.testDataDirty) {
// Set gauge rotations
downloadProgress.style = `--progress-rotation: ${
testState.testData.dlProgress * 180
}deg`;
uploadProgress.style = `--progress-rotation: ${
testState.testData.ulProgress * 180
}deg`;
downloadGauge.style = `--speed-rotation: ${mbpsToRotation(
testState.testData.dlStatus,
testState.testData.testState === 1
)}deg`;
uploadGauge.style = `--speed-rotation: ${mbpsToRotation(
testState.testData.ulStatus,
testState.testData.testState === 3
)}deg`;
// Set numeric values
downloadText.textContent = numberToText(testState.testData.dlStatus);
uploadText.textContent = numberToText(testState.testData.ulStatus);
ping.textContent = numberToText(testState.testData.pingStatus);
jitter.textContent = numberToText(testState.testData.jitterStatus);
// Set user's IP and provider
if (testState.testData.clientIp) {
// Clear previous content
privacyWarning.innerHTML = '';
const connectedThrough = document.createElement('span');
connectedThrough.textContent = 'You are connected through:';
const ipAddress = document.createTextNode(testState.testData.clientIp);
privacyWarning.appendChild(connectedThrough);
privacyWarning.appendChild(document.createElement('br'));
privacyWarning.appendChild(ipAddress);
privacyWarning.classList.remove("hidden");
}
// Set image for sharing results
if (testState.testData.testId) {
resultsImage.src =
window.location.href.substring(
0,
window.location.href.lastIndexOf("/")
) +
"/results/?id=" +
testState.testData.testId;
}
testState.testDataDirty = false;
}
requestAnimationFrame(renderUI);
}
renderUI();
}
/**
* Convert a speed in Mbits per second to a rotation for the gauge
* @param {string} speed Speed in Mbits
* @param {boolean} oscillate If the gauge should wiggle a bit
* @returns {number} Rotation for the gauge in degrees
*/
function mbpsToRotation(speed, oscillate) {
speed = Number(speed);
if (speed <= 0) return 0;
const minSpeed = 0;
const maxSpeed = 10000; // 10 Gbps maxes out the gauge
const minRotation = 0;
const maxRotation = 180;
// Can't do log10 of values less than one, +1 all to keep it fair
const logMinSpeed = Math.log10(minSpeed + 1);
const logMaxSpeed = Math.log10(maxSpeed + 1);
const logSpeed = Math.log10(speed + 1);
const power = (logSpeed - logMinSpeed) / (logMaxSpeed - logMinSpeed);
const oscillation = oscillate ? 1 + 0.01 * Math.sin(Date.now() / 100) : 1;
const rotation = power * oscillation * maxRotation;
// Make sure we stay within bounds at all times
return Math.max(Math.min(rotation, maxRotation), minRotation);
}
/**
* Convert a number to a user friendly version
* @param {string} value Speed, ping or jitter
* @returns {string} A text version with proper decimals
*/
function numberToText(value) {
if (!value) return "00";
value = Number(value);
if (value < 10) return value.toFixed(2);
if (value < 100) return value.toFixed(1);
return value.toFixed(0);
}
+51 -51
View File
@@ -6,15 +6,15 @@
*/ */
/* /*
This is the main interface between your webpage and the speedtest. This is the main interface between your webpage and the speed test.
It hides the speedtest web worker to the page, and provides many convenient functions to control the test. It hides the speed test web worker to the page, and provides many convenient functions to control the test.
The best way to learn how to use this is to look at the basic example, but here's some documentation. The best way to learn how to use this is to look at the basic example, but here's some documentation.
To initialize the test, create a new Speedtest object: To initialize the test, create a new Speedtest object:
var s=new Speedtest(); let s=new Speedtest();
Now you can think of this as a finite state machine. These are the states (use getState() to see them): Now you can think of this as a finite state machine. These are the states (use getState() to see them):
- 0: here you can change the speedtest settings (such as test duration) with the setParameter("parameter",value) method. From here you can either start the test using start() (goes to state 3) or you can add multiple test points using addTestPoint(server) or addTestPoints(serverList) (goes to state 1). Additionally, this is the perfect moment to set up callbacks for the onupdate(data) and onend(aborted) events. - 0: here you can change the speed test settings (such as test duration) with the setParameter("parameter",value) method. From here you can either start the test using start() (goes to state 3) or you can add multiple test points using addTestPoint(server) or addTestPoints(serverList) (goes to state 1). Additionally, this is the perfect moment to set up callbacks for the onupdate(data) and onend(aborted) events.
- 1: here you can add test points. You only need to do this if you want to use multiple test points. - 1: here you can add test points. You only need to do this if you want to use multiple test points.
A server is defined as an object like this: A server is defined as an object like this:
{ {
@@ -27,7 +27,7 @@
} }
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. 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 - 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: - 3: test running. Here, your onupdate event callback will be called periodically, with data coming from the worker about speed and progress. A data object will be passed to your onupdate function, with the following items:
- dlStatus: download speed in Mbit/s - dlStatus: download speed in Mbit/s
- ulStatus: upload speed in Mbit/s - ulStatus: upload speed in Mbit/s
- pingStatus: ping in ms - pingStatus: ping in ms
@@ -36,7 +36,7 @@
- ulProgress: progress of the upload test as a float 0-1 - ulProgress: progress of the upload test as a float 0-1
- pingProgress: progress of the ping/jitter test as a float 0-1 - pingProgress: progress of the ping/jitter test as a float 0-1
- testState: state of the test (-1=not started, 0=starting, 1=download test, 2=ping+jitter test, 3=upload test, 4=finished, 5=aborted) - testState: state of the test (-1=not started, 0=starting, 1=download test, 2=ping+jitter test, 3=upload test, 4=finished, 5=aborted)
- clientIp: IP address of the client performing the test (and optionally ISP and distance) - clientIp: IP address of the client performing the test (and optionally ISP and distance)
At the end of the test, the onend function will be called, with a boolean specifying whether the test was aborted or if it ended normally. At the end of the test, the onend function will be called, with a boolean specifying whether the test was aborted or if it ended normally.
The test can be aborted at any time with abort(). The test can be aborted at any time with abort().
At the end of the test, it will move to state 4 At the end of the test, it will move to state 4
@@ -46,10 +46,10 @@
function Speedtest() { function Speedtest() {
this._serverList = []; //when using multiple points of test, this is a list of test points this._serverList = []; //when using multiple points of test, this is a list of test points
this._selectedServer = null; //when using multiple points of test, this is the selected server this._selectedServer = null; //when using multiple points of test, this is the selected server
this._settings = {}; //settings for the speedtest worker this._settings = {}; //settings for the speed test worker
this._state = 0; //0=adding settings, 1=adding servers, 2=server selection done, 3=test running, 4=done this._state = 0; //0=adding settings, 1=adding servers, 2=server selection done, 3=test running, 4=done
console.log( console.log(
"LibreSpeed by Federico Dossena v5.2 - https://github.com/librespeed/speedtest" "LibreSpeed by Federico Dossena v6.1.0 - https://github.com/librespeed/speedtest"
); );
} }
@@ -66,13 +66,13 @@ Speedtest.prototype = {
* - parameter: string with the name of the parameter that you want to set * - parameter: string with the name of the parameter that you want to set
* - value: new value for the parameter * - value: new value for the parameter
* *
* Invalid values or nonexistant parameters will be ignored by the speedtest worker. * Invalid values or nonexistant parameters will be ignored by the speed test worker.
*/ */
setParameter: function(parameter, value) { setParameter: function(parameter, value) {
if (this._state != 0) if (this._state == 3)
throw "You cannot change the test settings after adding server or starting the test"; throw "You cannot change the test settings while running the test";
this._settings[parameter] = value; this._settings[parameter] = value;
if(parameter === "temeletry_extra"){ if(parameter === "telemetry_extra"){
this._originalExtra=this._settings.telemetry_extra; this._originalExtra=this._settings.telemetry_extra;
} }
}, },
@@ -125,7 +125,7 @@ Speedtest.prototype = {
* Same as addTestPoint, but you can pass an array of servers * Same as addTestPoint, but you can pass an array of servers
*/ */
addTestPoints: function(list) { addTestPoints: function(list) {
for (var i = 0; i < list.length; i++) this.addTestPoint(list[i]); for (let i = 0; i < list.length; i++) this.addTestPoint(list[i]);
}, },
/** /**
* Load a JSON server list from URL (multiple points of test) * Load a JSON server list from URL (multiple points of test)
@@ -144,11 +144,11 @@ Speedtest.prototype = {
if (this._state == 0) this._state = 1; if (this._state == 0) this._state = 1;
if (this._state != 1) throw "You can't add a server after server selection"; if (this._state != 1) throw "You can't add a server after server selection";
this._settings.mpot = true; this._settings.mpot = true;
var xhr = new XMLHttpRequest(); let xhr = new XMLHttpRequest();
xhr.onload = function(){ xhr.onload = function(){
try{ try{
var servers=JSON.parse(xhr.responseText); const servers=JSON.parse(xhr.responseText);
for(var i=0;i<servers.length;i++){ for(let i=0;i<servers.length;i++){
this._checkServerDefinition(servers[i]); this._checkServerDefinition(servers[i]);
} }
this.addTestPoints(servers); this.addTestPoints(servers);
@@ -191,37 +191,37 @@ Speedtest.prototype = {
throw "You can't select a server while the test is running"; throw "You can't select a server while the test is running";
} }
if (this._selectServerCalled) throw "selectServer already called"; else this._selectServerCalled=true; 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) { const select = function(serverList, selected) {
//pings the specified URL, then calls the function result. Result will receive a parameter which is either the time it took to ping the URL, or -1 if something went wrong. //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; const PING_TIMEOUT = 2000;
var USE_PING_TIMEOUT = true; //will be disabled on unsupported browsers let USE_PING_TIMEOUT = true; //will be disabled on unsupported browsers
if (/MSIE.(\d+\.\d+)/i.test(navigator.userAgent)) { if (/MSIE.(\d+\.\d+)/i.test(navigator.userAgent)) {
//IE11 doesn't support XHR timeout //IE11 doesn't support XHR timeout
USE_PING_TIMEOUT = false; USE_PING_TIMEOUT = false;
} }
var ping = function(url, result) { const ping = function(url, rtt) {
url += (url.match(/\?/) ? "&" : "?") + "cors=true"; url += (url.match(/\?/) ? "&" : "?") + "cors=true";
var xhr = new XMLHttpRequest(); let xhr = new XMLHttpRequest();
var t = new Date().getTime(); let t = new Date().getTime();
xhr.onload = function() { xhr.onload = function() {
if (xhr.responseText.length == 0) { if (xhr.responseText.length == 0) {
//we expect an empty response //we expect an empty response
var instspd = new Date().getTime() - t; //rough timing estimate let instspd = new Date().getTime() - t; //rough timing estimate
try { try {
//try to get more accurate timing using performance API //try to get more accurate timing using performance API
var p = performance.getEntriesByName(url); let p = performance.getEntriesByName(url);
p = p[p.length - 1]; p = p[p.length - 1];
var d = p.responseStart - p.requestStart; let d = p.responseStart - p.requestStart;
if (d <= 0) d = p.duration; if (d <= 0) d = p.duration;
if (d > 0 && d < instspd) instspd = d; if (d > 0 && d < instspd) instspd = d;
} catch (e) {} } catch (e) {}
result(instspd); rtt(instspd);
} else result(-1); } else rtt(-1);
}.bind(this); }.bind(this);
xhr.onerror = function() { xhr.onerror = function() {
result(-1); rtt(-1);
}.bind(this); }.bind(this);
xhr.open("GET", url); xhr.open("GET", url);
if (USE_PING_TIMEOUT) { if (USE_PING_TIMEOUT) {
@@ -234,14 +234,14 @@ Speedtest.prototype = {
}.bind(this); }.bind(this);
//this function repeatedly pings a server to get a good estimate of the ping. When it's done, it calls the done function without parameters. At the end of the execution, the server will have a new parameter called pingT, which is either the best ping we got from the server or -1 if something went wrong. //this function repeatedly pings a server to get a good estimate of the ping. When it's done, it calls the done function without parameters. At the end of the execution, the server will have a new parameter called pingT, which is either the best ping we got from the server or -1 if something went wrong.
var PINGS = 3, //up to 3 pings are performed, unless the server is down... const PINGS = 3, //up to 3 pings are performed, unless the server is down...
SLOW_THRESHOLD = 500; //...or one of the pings is above this threshold SLOW_THRESHOLD = 500; //...or one of the pings is above this threshold
var checkServer = function(server, done) { const checkServer = function(server, done) {
var i = 0; let i = 0;
server.pingT = -1; server.pingT = -1;
if (server.server.indexOf(location.protocol) == -1) done(); if (server.server.indexOf(location.protocol) == -1) done();
else { else {
var nextPing = function() { const nextPing = function() {
if (i++ == PINGS) { if (i++ == PINGS) {
done(); done();
return; return;
@@ -261,19 +261,19 @@ Speedtest.prototype = {
} }
}.bind(this); }.bind(this);
//check servers in list, one by one //check servers in list, one by one
var i = 0; let i = 0;
var done = function() { const done = function() {
var bestServer = null; let bestServer = null;
for (var i = 0; i < serverList.length; i++) { for (let i = 0; i < serverList.length; i++) {
if ( if (
serverList[i].pingT != -1 && serverList[i].pingT != -1 &&
(bestServer == null || serverList[i].pingT < bestServer.pingT) (bestServer == null || serverList[i].pingT < bestServer.pingT)
) )
bestServer = serverList[i]; bestServer = serverList[i];
} }
result(bestServer); selected(bestServer);
}.bind(this); }.bind(this);
var nextServer = function() { const nextServer = function() {
if (i == serverList.length) { if (i == serverList.length) {
done(); done();
return; return;
@@ -284,17 +284,17 @@ Speedtest.prototype = {
}.bind(this); }.bind(this);
//parallel server selection //parallel server selection
var CONCURRENCY = 6; const CONCURRENCY = 6;
var serverLists = []; let serverLists = [];
for (var i = 0; i < CONCURRENCY; i++) { for (let i = 0; i < CONCURRENCY; i++) {
serverLists[i] = []; serverLists[i] = [];
} }
for (var i = 0; i < this._serverList.length; i++) { for (let i = 0; i < this._serverList.length; i++) {
serverLists[i % CONCURRENCY].push(this._serverList[i]); serverLists[i % CONCURRENCY].push(this._serverList[i]);
} }
var completed = 0; let completed = 0;
var bestServer = null; let bestServer = null;
for (var i = 0; i < CONCURRENCY; i++) { for (let i = 0; i < CONCURRENCY; i++) {
select( select(
serverLists[i], serverLists[i],
function(server) { function(server) {
@@ -323,20 +323,20 @@ Speedtest.prototype = {
this.worker.onmessage = function(e) { this.worker.onmessage = function(e) {
if (e.data === this._prevData) return; if (e.data === this._prevData) return;
else this._prevData = e.data; else this._prevData = e.data;
var data = JSON.parse(e.data); const data = JSON.parse(e.data);
try { try {
if (this.onupdate) this.onupdate(data); if (this.onupdate) this.onupdate(data);
} catch (e) { } catch (e) {
console.error("Speedtest onupdate event threw exception: " + e); console.error("Speedtest onupdate event threw exception: " + e);
} }
if (data.testState >= 4) { if (data.testState >= 4) {
clearInterval(this.updater);
this._state = 4;
try { try {
if (this.onend) this.onend(data.testState == 5); if (this.onend) this.onend(data.testState == 5);
} catch (e) { } catch (e) {
console.error("Speedtest onend event threw exception: " + e); console.error("Speedtest onend event threw exception: " + e);
} }
clearInterval(this.updater);
this._state = 4;
} }
}.bind(this); }.bind(this);
this.updater = setInterval( this.updater = setInterval(
+86 -85
View File
@@ -6,18 +6,18 @@
*/ */
// data reported to main thread // 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 let 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 let dlStatus = ""; // download speed in megabit/s with 2 decimal digits
var ulStatus = ""; // upload speed in Mbit/s with 2 decimal digits let ulStatus = ""; // upload speed in megabit/s with 2 decimal digits
var pingStatus = ""; // ping in milliseconds with 2 decimal digits let pingStatus = ""; // ping in milliseconds with 2 decimal digits
var jitterStatus = ""; // jitter in milliseconds with 2 decimal digits let jitterStatus = ""; // jitter in milliseconds with 2 decimal digits
var clientIp = ""; // client's IP address as reported by getIP let clientIp = ""; // client's IP address as reported by getIP.php
var dlProgress = 0; //progress of download test 0-1 let dlProgress = 0; //progress of download test 0-1
var ulProgress = 0; //progress of upload test 0-1 let ulProgress = 0; //progress of upload test 0-1
var pingProgress = 0; //progress of ping+jitter test 0-1 let pingProgress = 0; //progress of ping+jitter test 0-1
var testId = null; //test ID (sent back by telemetry if used, null otherwise) let testId = null; //test ID (sent back by telemetry if used, null otherwise)
var log = ""; //telemetry log let log = ""; //telemetry log
function tlog(s) { function tlog(s) {
if (settings.telemetry_level >= 2) { if (settings.telemetry_level >= 2) {
log += Date.now() + ": " + s + "\n"; log += Date.now() + ": " + s + "\n";
@@ -36,7 +36,7 @@ function twarn(s) {
} }
// test settings. can be overridden by sending specific values with the start command // test settings. can be overridden by sending specific values with the start command
var settings = { let settings = {
mpot: false, //set to true when in MPOT mode mpot: false, //set to true when in MPOT mode
test_order: "IP_D_U", //order in which tests will be performed as a string. D=Download, U=Upload, P=Ping+Jitter, I=IP, _=1 second delay test_order: "IP_D_U", //order in which tests will be performed as a string. D=Download, U=Upload, P=Ping+Jitter, I=IP, _=1 second delay
time_ul_max: 15, // max duration of upload test in seconds time_ul_max: 15, // max duration of upload test in seconds
@@ -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_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) 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 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_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", // path to an empty file, used for upload 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", // path to an empty file, used for ping 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", // path to getIP.php relative to this js file, or a similar thing that outputs the client's ip 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: 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 getIp_ispInfo_distance: "km", //km or mi=estimate distance from server in km/mi; set to false to disable distance estimation. getIp_ispInfo must be enabled in order for this to work
xhr_dlMultistream: 6, // number of download streams to use (can be different if enable_quirks is active) xhr_dlMultistream: 6, // number of download streams to use (can be different if enable_quirks is active)
@@ -60,16 +60,17 @@ var settings = {
garbagePhp_chunkSize: 100, // size of chunks sent by garbage.php (can be different if enable_quirks is active) garbagePhp_chunkSize: 100, // size of chunks sent by garbage.php (can be different if enable_quirks is active)
enable_quirks: true, // enable quirks for specific browsers. currently it overrides settings to optimize for specific browsers, unless they are already being overridden with the start command 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. ping_allowPerformanceApi: true, // if enabled, the ping test will attempt to calculate the ping more precisely using the Performance API. Currently works perfectly in Chrome, badly in Edge, and not at all in Firefox. If Performance API is not supported or the result is obviously wrong, a fallback is provided.
overheadCompensationFactor: 1.06, //can be changed to compensatie for transport overhead. (see doc.md for some other values) overheadCompensationFactor: 1.06, //can be changed to compensate for transport overhead. (see doc.md for some other values)
useMebibits: false, //if set to true, speed will be reported in 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) 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 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 telemetry_extra: "", //extra data that can be passed to the telemetry through the settings
forceIE11Workaround: false //when set to true, it will force the IE11 upload test on all browsers. Debug only
}; };
var xhr = null; // array of currently active xhr requests let xhr = null; // array of currently active xhr requests
var interval = null; // timer used in tests let interval = null; // timer used in tests
var test_pointer = 0; //pointer to the next test to run inside settings.test_order let test_pointer = 0; //pointer to the next test to run inside settings.test_order
/* /*
this function is used on URLs passed in the settings to determine whether we need a ? or an & as a separator this function is used on URLs passed in the settings to determine whether we need a ? or an & as a separator
@@ -87,7 +88,7 @@ function url_sep(url) {
example: start {"time_ul_max":"10", "time_dl_max":"10", "count_ping":"50"} example: start {"time_ul_max":"10", "time_dl_max":"10", "count_ping":"50"}
*/ */
this.addEventListener("message", function(e) { this.addEventListener("message", function(e) {
var params = e.data.split(" "); const params = e.data.split(" ");
if (params[0] === "status") { if (params[0] === "status") {
// return status // return status
postMessage( postMessage(
@@ -110,19 +111,19 @@ this.addEventListener("message", function(e) {
testState = 0; testState = 0;
try { try {
// parse settings, if present // parse settings, if present
var s = {}; let s = {};
try { try {
var ss = e.data.substring(5); const ss = e.data.substring(5);
if (ss) s = JSON.parse(ss); if (ss) s = JSON.parse(ss);
} catch (e) { } catch (e) {
twarn("Error parsing custom settings JSON. Please check your syntax"); twarn("Error parsing custom settings JSON. Please check your syntax");
} }
//copy custom settings //copy custom settings
for (var key in s) { for (let key in s) {
if (typeof settings[key] !== "undefined") settings[key] = s[key]; if (typeof settings[key] !== "undefined") settings[key] = s[key];
else twarn("Unknown setting ignored: " + key); else twarn("Unknown setting ignored: " + key);
} }
var ua = navigator.userAgent; const ua = navigator.userAgent;
// quirks for specific browsers. apply only if not overridden. more may be added in future releases // quirks for specific browsers. apply only if not overridden. more may be added in future releases
if (settings.enable_quirks || (typeof s.enable_quirks !== "undefined" && s.enable_quirks)) { if (settings.enable_quirks || (typeof s.enable_quirks !== "undefined" && s.enable_quirks)) {
if (/Firefox.(\d+\.\d+)/i.test(ua)) { if (/Firefox.(\d+\.\d+)/i.test(ua)) {
@@ -171,11 +172,11 @@ this.addEventListener("message", function(e) {
// run the tests // run the tests
tverb(JSON.stringify(settings)); tverb(JSON.stringify(settings));
test_pointer = 0; test_pointer = 0;
var iRun = false, let iRun = false,
dRun = false, dRun = false,
uRun = false, uRun = false,
pRun = false; pRun = false;
var runNextTest = function() { const runNextTest = function() {
if (testState == 5) return; if (testState == 5) return;
if (test_pointer >= settings.test_order.length) { if (test_pointer >= settings.test_order.length) {
//test is finished //test is finished
@@ -266,7 +267,7 @@ this.addEventListener("message", function(e) {
function clearRequests() { function clearRequests() {
tverb("stopping pending XHRs"); tverb("stopping pending XHRs");
if (xhr) { if (xhr) {
for (var i = 0; i < xhr.length; i++) { for (let i = 0; i < xhr.length; i++) {
try { try {
xhr[i].onprogress = null; xhr[i].onprogress = null;
xhr[i].onload = null; xhr[i].onload = null;
@@ -288,18 +289,18 @@ function clearRequests() {
} }
} }
// gets client's IP using url_getIp, then calls the done function // gets client's IP using url_getIp, then calls the done function
var ipCalled = false; // used to prevent multiple accidental calls to getIp let ipCalled = false; // used to prevent multiple accidental calls to getIp
var ispInfo = ""; //used for telemetry let ispInfo = ""; //used for telemetry
function getIp(done) { function getIp(done) {
tverb("getIp"); tverb("getIp");
if (ipCalled) return; if (ipCalled) return;
else ipCalled = true; // getIp already called? else ipCalled = true; // getIp already called?
var startT = new Date().getTime(); let startT = new Date().getTime();
xhr = new XMLHttpRequest(); xhr = new XMLHttpRequest();
xhr.onload = function() { xhr.onload = function() {
tlog("IP: " + xhr.responseText + ", took " + (new Date().getTime() - startT) + "ms"); tlog("IP: " + xhr.responseText + ", took " + (new Date().getTime() - startT) + "ms");
try { try {
var data = JSON.parse(xhr.responseText); const data = JSON.parse(xhr.responseText);
clientIp = data.processedString; clientIp = data.processedString;
ispInfo = data.rawIspInfo; ispInfo = data.rawIspInfo;
} catch (e) { } catch (e) {
@@ -316,25 +317,25 @@ function getIp(done) {
xhr.send(); xhr.send();
} }
// download test, calls done function when it's over // download test, calls done function when it's over
var dlCalled = false; // used to prevent multiple accidental calls to dlTest let dlCalled = false; // used to prevent multiple accidental calls to dlTest
function dlTest(done) { function dlTest(done) {
tverb("dlTest"); tverb("dlTest");
if (dlCalled) return; if (dlCalled) return;
else dlCalled = true; // dlTest already called? else dlCalled = true; // dlTest already called?
var totLoaded = 0.0, // total number of loaded bytes let totLoaded = 0.0, // total number of loaded bytes
startT = new Date().getTime(), // timestamp when test was started startT = new Date().getTime(), // timestamp when test was started
bonusT = 0, //how many milliseconds the test has been shortened by (higher on faster connections) bonusT = 0, //how many milliseconds the test has been shortened by (higher on faster connections)
graceTimeDone = false, //set to true after the grace time is past graceTimeDone = false, //set to true after the grace time is past
failed = false; // set to true if a stream fails failed = false; // set to true if a stream fails
xhr = []; xhr = [];
// function to create a download stream. streams are slightly delayed so that they will not end at the same time // function to create a download stream. streams are slightly delayed so that they will not end at the same time
var testStream = function(i, delay) { const testStream = function(i, delay) {
setTimeout( setTimeout(
function() { function() {
if (testState !== 1) return; // delayed stream ended up starting after the end of the download test if (testState !== 1) return; // delayed stream ended up starting after the end of the download test
tverb("dl test stream started " + i + " " + delay); tverb("dl test stream started " + i + " " + delay);
var prevLoaded = 0; // number of bytes loaded last time onprogress was called let prevLoaded = 0; // number of bytes loaded last time onprogress was called
var x = new XMLHttpRequest(); let x = new XMLHttpRequest();
xhr[i] = x; xhr[i] = x;
xhr[i].onprogress = function(event) { xhr[i].onprogress = function(event) {
tverb("dl stream progress event " + i + " " + event.loaded); tverb("dl stream progress event " + i + " " + event.loaded);
@@ -344,7 +345,7 @@ function dlTest(done) {
} catch (e) {} } catch (e) {}
} // just in case this XHR is still running after the download test } // just in case this XHR is still running after the download test
// progress event, add number of new loaded bytes to totLoaded // progress event, add number of new loaded bytes to totLoaded
var loadDiff = event.loaded <= 0 ? 0 : event.loaded - prevLoaded; const loadDiff = event.loaded <= 0 ? 0 : event.loaded - prevLoaded;
if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return; // just in case if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return; // just in case
totLoaded += loadDiff; totLoaded += loadDiff;
prevLoaded = event.loaded; prevLoaded = event.loaded;
@@ -379,14 +380,14 @@ function dlTest(done) {
); );
}.bind(this); }.bind(this);
// open streams // open streams
for (var i = 0; i < settings.xhr_dlMultistream; i++) { for (let i = 0; i < settings.xhr_dlMultistream; i++) {
testStream(i, settings.xhr_multistreamDelay * i); testStream(i, settings.xhr_multistreamDelay * i);
} }
// every 200ms, update dlStatus // every 200ms, update dlStatus
interval = setInterval( interval = setInterval(
function() { function() {
tverb("DL: " + dlStatus + (graceTimeDone ? "" : " (in grace time)")); tverb("DL: " + dlStatus + (graceTimeDone ? "" : " (in grace time)"));
var t = new Date().getTime() - startT; const t = new Date().getTime() - startT;
if (graceTimeDone) dlProgress = (t + bonusT) / (settings.time_dl_max * 1000); if (graceTimeDone) dlProgress = (t + bonusT) / (settings.time_dl_max * 1000);
if (t < 200) return; if (t < 200) return;
if (!graceTimeDone) { if (!graceTimeDone) {
@@ -400,11 +401,11 @@ function dlTest(done) {
graceTimeDone = true; graceTimeDone = true;
} }
} else { } else {
var speed = totLoaded / (t / 1000.0); const speed = totLoaded / (t / 1000.0);
if (settings.time_auto) { if (settings.time_auto) {
//decide how much to shorten the test. Every 200ms, the test is shortened by the bonusT calculated here //decide how much to shorten the test. Every 200ms, the test is shortened by the bonusT calculated here
var bonus = (6.4 * speed) / 100000; const bonus = (5.0 * speed) / 100000;
bonusT += bonus > 800 ? 800 : bonus; bonusT += bonus > 400 ? 400 : bonus;
} }
//update status //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 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
@@ -422,47 +423,47 @@ function dlTest(done) {
200 200
); );
} }
// upload test, calls done function whent it's over // upload test, calls done function when it's over
var ulCalled = false; // used to prevent multiple accidental calls to ulTest let ulCalled = false; // used to prevent multiple accidental calls to ulTest
function ulTest(done) { function ulTest(done) {
tverb("ulTest"); tverb("ulTest");
if (ulCalled) return; if (ulCalled) return;
else ulCalled = true; // ulTest already called? else ulCalled = true; // ulTest already called?
// garbage data for upload test // garbage data for upload test
var r = new ArrayBuffer(1048576); let r = new ArrayBuffer(1048576);
var maxInt = Math.pow(2, 32) - 1; const maxInt = Math.pow(2, 32) - 1;
try { try {
r = new Uint32Array(r); r = new Uint32Array(r);
for (var i = 0; i < r.length; i++) r[i] = Math.random() * maxInt; for (let i = 0; i < r.length; i++) r[i] = Math.random() * maxInt;
} catch (e) {} } catch (e) {}
var req = []; let req = [];
var reqsmall = []; let reqsmall = [];
for (var i = 0; i < settings.xhr_ul_blob_megabytes; i++) req.push(r); for (let i = 0; i < settings.xhr_ul_blob_megabytes; i++) req.push(r);
req = new Blob(req); req = new Blob(req);
r = new ArrayBuffer(262144); r = new ArrayBuffer(262144);
try { try {
r = new Uint32Array(r); r = new Uint32Array(r);
for (var i = 0; i < r.length; i++) r[i] = Math.random() * maxInt; for (let i = 0; i < r.length; i++) r[i] = Math.random() * maxInt;
} catch (e) {} } catch (e) {}
reqsmall.push(r); reqsmall.push(r);
reqsmall = new Blob(reqsmall); reqsmall = new Blob(reqsmall);
var testFunction = function() { const testFunction = function() {
var totLoaded = 0.0, // total number of transmitted bytes let totLoaded = 0.0, // total number of transmitted bytes
startT = new Date().getTime(), // timestamp when test was started startT = new Date().getTime(), // timestamp when test was started
bonusT = 0, //how many milliseconds the test has been shortened by (higher on faster connections) bonusT = 0, //how many milliseconds the test has been shortened by (higher on faster connections)
graceTimeDone = false, //set to true after the grace time is past graceTimeDone = false, //set to true after the grace time is past
failed = false; // set to true if a stream fails failed = false; // set to true if a stream fails
xhr = []; xhr = [];
// function to create an upload stream. streams are slightly delayed so that they will not end at the same time // function to create an upload stream. streams are slightly delayed so that they will not end at the same time
var testStream = function(i, delay) { const testStream = function(i, delay) {
setTimeout( setTimeout(
function() { function() {
if (testState !== 3) return; // delayed stream ended up starting after the end of the upload test if (testState !== 3) return; // delayed stream ended up starting after the end of the upload test
tverb("ul test stream started " + i + " " + delay); tverb("ul test stream started " + i + " " + delay);
var prevLoaded = 0; // number of bytes transmitted last time onprogress was called let prevLoaded = 0; // number of bytes transmitted last time onprogress was called
var x = new XMLHttpRequest(); let x = new XMLHttpRequest();
xhr[i] = x; xhr[i] = x;
var ie11workaround; let ie11workaround;
if (settings.forceIE11Workaround) ie11workaround = true; if (settings.forceIE11Workaround) ie11workaround = true;
else { else {
try { try {
@@ -473,7 +474,7 @@ function ulTest(done) {
} }
} }
if (ie11workaround) { if (ie11workaround) {
// IE11 workarond: xhr.upload does not work properly, therefore we send a bunch of small 256k requests and use the onload event as progress. This is not precise, especially on fast connections // IE11 workaround: xhr.upload does not work properly, therefore we send a bunch of small 256k requests and use the onload event as progress. This is not precise, especially on fast connections
xhr[i].onload = xhr[i].onerror = function() { xhr[i].onload = xhr[i].onerror = function() {
tverb("ul stream progress event (ie11wa)"); tverb("ul stream progress event (ie11wa)");
totLoaded += reqsmall.size; totLoaded += reqsmall.size;
@@ -495,7 +496,7 @@ function ulTest(done) {
} catch (e) {} } catch (e) {}
} // just in case this XHR is still running after the upload test } // just in case this XHR is still running after the upload test
// progress event, add number of new loaded bytes to totLoaded // progress event, add number of new loaded bytes to totLoaded
var loadDiff = event.loaded <= 0 ? 0 : event.loaded - prevLoaded; const loadDiff = event.loaded <= 0 ? 0 : event.loaded - prevLoaded;
if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return; // just in case if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return; // just in case
totLoaded += loadDiff; totLoaded += loadDiff;
prevLoaded = event.loaded; prevLoaded = event.loaded;
@@ -527,14 +528,14 @@ function ulTest(done) {
); );
}.bind(this); }.bind(this);
// open streams // open streams
for (var i = 0; i < settings.xhr_ulMultistream; i++) { for (let i = 0; i < settings.xhr_ulMultistream; i++) {
testStream(i, settings.xhr_multistreamDelay * i); testStream(i, settings.xhr_multistreamDelay * i);
} }
// every 200ms, update ulStatus // every 200ms, update ulStatus
interval = setInterval( interval = setInterval(
function() { function() {
tverb("UL: " + ulStatus + (graceTimeDone ? "" : " (in grace time)")); tverb("UL: " + ulStatus + (graceTimeDone ? "" : " (in grace time)"));
var t = new Date().getTime() - startT; const t = new Date().getTime() - startT;
if (graceTimeDone) ulProgress = (t + bonusT) / (settings.time_ul_max * 1000); if (graceTimeDone) ulProgress = (t + bonusT) / (settings.time_ul_max * 1000);
if (t < 200) return; if (t < 200) return;
if (!graceTimeDone) { if (!graceTimeDone) {
@@ -548,11 +549,11 @@ function ulTest(done) {
graceTimeDone = true; graceTimeDone = true;
} }
} else { } else {
var speed = totLoaded / (t / 1000.0); const speed = totLoaded / (t / 1000.0);
if (settings.time_auto) { if (settings.time_auto) {
//decide how much to shorten the test. Every 200ms, the test is shortened by the bonusT calculated here //decide how much to shorten the test. Every 200ms, the test is shortened by the bonusT calculated here
var bonus = (6.4 * speed) / 100000; const bonus = (5.0 * speed) / 100000;
bonusT += bonus > 800 ? 800 : bonus; bonusT += bonus > 400 ? 400 : bonus;
} }
//update status //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 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
@@ -583,20 +584,20 @@ function ulTest(done) {
} else testFunction(); } else testFunction();
} }
// ping+jitter test, function done is called when it's over // ping+jitter test, function done is called when it's over
var ptCalled = false; // used to prevent multiple accidental calls to pingTest let ptCalled = false; // used to prevent multiple accidental calls to pingTest
function pingTest(done) { function pingTest(done) {
tverb("pingTest"); tverb("pingTest");
if (ptCalled) return; if (ptCalled) return;
else ptCalled = true; // pingTest already called? else ptCalled = true; // pingTest already called?
var startT = new Date().getTime(); //when the test was started const startT = new Date().getTime(); //when the test was started
var prevT = null; // last time a pong was received let prevT = null; // last time a pong was received
var ping = 0.0; // current ping value let ping = 0.0; // current ping value
var jitter = 0.0; // current jitter value let jitter = 0.0; // current jitter value
var i = 0; // counter of pongs received let i = 0; // counter of pongs received
var prevInstspd = 0; // last ping time, used for jitter calculation let prevInstspd = 0; // last ping time, used for jitter calculation
xhr = []; xhr = [];
// ping function // ping function
var doPing = function() { const doPing = function() {
tverb("ping"); tverb("ping");
pingProgress = i / settings.count_ping; pingProgress = i / settings.count_ping;
prevT = new Date().getTime(); prevT = new Date().getTime();
@@ -607,13 +608,13 @@ function pingTest(done) {
if (i === 0) { if (i === 0) {
prevT = new Date().getTime(); // first pong prevT = new Date().getTime(); // first pong
} else { } else {
var instspd = new Date().getTime() - prevT; let instspd = new Date().getTime() - prevT;
if (settings.ping_allowPerformanceApi) { if (settings.ping_allowPerformanceApi) {
try { try {
//try to get accurate performance timing using performance api //try to get accurate performance timing using performance api
var p = performance.getEntries(); let p = performance.getEntries();
p = p[p.length - 1]; p = p[p.length - 1];
var d = p.responseStart - p.requestStart; let d = p.responseStart - p.requestStart;
if (d <= 0) d = p.duration; if (d <= 0) d = p.duration;
if (d > 0 && d < instspd) instspd = d; if (d > 0 && d < instspd) instspd = d;
} catch (e) { } catch (e) {
@@ -624,7 +625,7 @@ function pingTest(done) {
//noticed that some browsers randomly have 0ms ping //noticed that some browsers randomly have 0ms ping
if (instspd < 1) instspd = prevInstspd; if (instspd < 1) instspd = prevInstspd;
if (instspd < 1) instspd = 1; if (instspd < 1) instspd = 1;
var instjitter = Math.abs(instspd - prevInstspd); const instjitter = Math.abs(instspd - prevInstspd);
if (i === 1) ping = instspd; if (i === 1) ping = instspd;
/* first ping, can't tell jitter yet*/ else { /* first ping, can't tell jitter yet*/ else {
if (instspd < ping) ping = instspd; // update ping, if the instant ping is lower if (instspd < ping) ping = instspd; // update ping, if the instant ping is lower
@@ -683,10 +684,10 @@ function sendTelemetry(done) {
xhr = new XMLHttpRequest(); xhr = new XMLHttpRequest();
xhr.onload = function() { xhr.onload = function() {
try { try {
var parts = xhr.responseText.split(" "); const parts = xhr.responseText.split(" ");
if (parts[0] == "id") { if (parts[0] == "id") {
try { try {
var id = parts[1]; let id = parts[1];
done(id); done(id);
} catch (e) { } catch (e) {
done(null); done(null);
@@ -701,12 +702,12 @@ function sendTelemetry(done) {
done(null); done(null);
}; };
xhr.open("POST", settings.url_telemetry + url_sep(settings.url_telemetry) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); xhr.open("POST", settings.url_telemetry + url_sep(settings.url_telemetry) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true);
var telemetryIspInfo = { const telemetryIspInfo = {
processedString: clientIp, processedString: clientIp,
rawIspInfo: typeof ispInfo === "object" ? ispInfo : "" rawIspInfo: typeof ispInfo === "object" ? ispInfo : ""
}; };
try { try {
var fd = new FormData(); const fd = new FormData();
fd.append("ispinfo", JSON.stringify(telemetryIspInfo)); fd.append("ispinfo", JSON.stringify(telemetryIspInfo));
fd.append("dl", dlStatus); fd.append("dl", dlStatus);
fd.append("ul", ulStatus); fd.append("ul", ulStatus);
@@ -716,7 +717,7 @@ function sendTelemetry(done) {
fd.append("extra", settings.telemetry_extra); fd.append("extra", settings.telemetry_extra);
xhr.send(fd); xhr.send(fd);
} catch (ex) { } catch (ex) {
var postData = "extra=" + encodeURIComponent(settings.telemetry_extra) + "&ispinfo=" + encodeURIComponent(JSON.stringify(telemetryIspInfo)) + "&dl=" + encodeURIComponent(dlStatus) + "&ul=" + encodeURIComponent(ulStatus) + "&ping=" + encodeURIComponent(pingStatus) + "&jitter=" + encodeURIComponent(jitterStatus) + "&log=" + encodeURIComponent(settings.telemetry_level > 1 ? log : ""); const postData = "extra=" + encodeURIComponent(settings.telemetry_extra) + "&ispinfo=" + encodeURIComponent(JSON.stringify(telemetryIspInfo)) + "&dl=" + encodeURIComponent(dlStatus) + "&ul=" + encodeURIComponent(ulStatus) + "&ping=" + encodeURIComponent(pingStatus) + "&jitter=" + encodeURIComponent(jitterStatus) + "&log=" + encodeURIComponent(settings.telemetry_level > 1 ? log : "");
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send(postData); xhr.send(postData);
} }
+83
View File
@@ -0,0 +1,83 @@
/* The main "start the test" button and the share button */
button {
height: 6.8rem;
min-width: 26.4rem;
padding: 0 5rem;
margin: 2.5rem;
border-radius: 3.4rem;
border: 0;
font-family: "Inter", sans-serif;
font-size: 2rem;
font-weight: 700;
letter-spacing: -0.1rem;
color: var(--button-text-color);
text-transform: uppercase;
cursor: pointer;
box-shadow: 0 0.4rem 1.6rem 0 var(--button-shadow-color);
will-change: transform;
backface-visibility: hidden;
transform: scale(1) translate3d(0, 0, 0) perspective(1px);
background: var(--button-gradient-1-color-1);
transition: background-position 0.2s, transform 0.2s;
background-position: 0% 0%;
background: linear-gradient(
92.97deg,
var(--button-gradient-1-color-1) 0%,
var(--button-gradient-1-color-1) 33%,
var(--button-gradient-1-color-2) 40%,
var(--button-gradient-1-color-3) 66.71%,
var(--button-gradient-1-color-3) 100%
);
background-size: 300% 100%;
&.disabled {
cursor: default;
transform: scale(1) translate3d(0, 0, 0) perspective(1px);
background: var(--button-disabled-background-color);
}
&.small {
height: 4.7rem;
min-width: 20.2rem;
text-transform: lowercase;
}
&.inverted {
border: 1px solid var(--button-gradient-1-color-1);
color: transparent;
background-clip: text;
}
&.hidden {
opacity: 0;
pointer-events: none;
}
&:hover {
background-position: 60% 0%;
transform: scale(1.03) translate3d(0, 0, 0) perspective(1px);
}
&.active,
&:active {
background-position: 100% 0%;
animation: pulse 0.7s;
}
}
@keyframes pulse {
0% {
transform: scale(1.03) translate3d(0, 0, 0) perspective(1px);
}
20% {
transform: scale(1.2) translate3d(0, 0, 0) perspective(1px);
}
40% {
transform: scale(1) translate3d(0, 0, 0) perspective(1px);
}
60% {
transform: scale(1.1) translate3d(0, 0, 0) perspective(1px);
}
100% {
transform: scale(1) translate3d(0, 0, 0) perspective(1px);
}
}
+36
View File
@@ -0,0 +1,36 @@
:root {
--theme-green: #5cf9fd;
--theme-pink: #d63bc6;
--background-backup-color: #0e0720;
--background-overlay-color: rgb(41 26 70 / 71%);
--primary-text-color: #ffffff;
--tagline-text-color: var(--theme-green);
--secondary-text-color: #898591;
--primary-text-disabled-color: #888888;
--secondary-text-disabled-color: #2e7d7f;
--button-text-color: #3e2f50;
--button-gradient-1-color-1: #f5f5f5;
--button-gradient-1-color-2: var(--theme-green);
--button-gradient-1-color-3: var(--theme-pink);
--button-shadow-color: #5cf9fd47;
--button-disabled-background-color: #a2a2a2;
--server-selector-border-color: #625b6b;
--server-selector-hover-border-color: var(--theme-green);
--server-selector-background-color: #251b32;
--server-selector-hover-background-color: var(--server-selector-border-color);
--gauge-background-color: #3e2f50;
--gauge-progress-color: #726c7a;
--gauge-pointer-green: #e2fbfc;
--gauge-pointer-pink: #d091ca;
--ping-and-jitter-primary-text-color: #f5f5f5;
--ping-and-jitter-secondary-text-color: #7b7b7b;
--popup-background-color: #251b32;
--popup-shadow-color: #000000;
}
+132
View File
@@ -0,0 +1,132 @@
/* Styling for the popups */
dialog {
flex-direction: column;
align-items: center;
justify-content: center;
width: 70vw;
height: 70vh;
margin: auto;
margin-top: 23rem;
background: var(--popup-background-color);
border: none;
border-radius: 0.8rem;
@media screen and (max-width: 800px) {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
max-width: 100vw; /* We need these overrides of browser defaults*/
max-height: 100vh;
width: auto;
height: auto;
margin: 0;
}
animation: fade-out 0.3s ease-out;
&[open] {
display: flex;
animation: fade-in 0.3s ease-out;
}
& > .close-dialog {
display: flex;
align-items: center;
justify-content: center;
width: 4rem;
height: 4rem;
position: absolute;
top: 3rem;
right: 3rem;
cursor: pointer;
}
& > section {
max-width: 800px;
overflow-y: auto;
margin: 4rem 2rem 2rem 4rem;
padding: 0 2rem 0 0;
& h1,
& h2 {
margin: 3rem 0 2rem 0;
font-size: 3.6rem;
font-weight: 400;
letter-spacing: -0.2rem;
color: var(--primary-text-color);
}
& h2 {
margin: 2rem 0 1rem 0;
font-size: 2.5rem;
}
& p,
& li {
margin: 1rem 0 1rem 0;
font-size: 1.6rem;
line-height: 2.5rem;
font-weight: 400;
letter-spacing: -0.1rem;
color: var(--secondary-text-color);
}
& ul {
list-style-position: inside;
margin: 1rem;
& li {
margin: 0.1rem 0;
}
}
& a {
font-size: 1.6rem;
font-weight: 700;
letter-spacing: -0.1rem;
color: var(--secondary-text-color);
text-underline-offset: 0.3rem;
transition: text-underline-offset 0.2s;
&:hover {
color: var(--theme-green);
text-underline-offset: 0.5rem;
}
}
}
}
@keyframes fade-in {
0% {
opacity: 0;
transform: scale(0.6);
display: none;
}
0.1% {
display: flex;
}
100% {
opacity: 1;
transform: scale(1);
display: flex;
}
}
@keyframes fade-out {
0% {
opacity: 1;
transform: scale(1);
display: flex;
}
99.9% {
display: flex;
}
100% {
opacity: 0;
transform: scale(0.6);
display: none;
}
}
+22
View File
@@ -0,0 +1,22 @@
/* latin-ext */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(../fonts/Inter-latin-ext.woff2) format("woff2");
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(../fonts/Inter-latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
+85
View File
@@ -0,0 +1,85 @@
/**
* Design by fromScratch Studio - 2022, 2023 (fromscratch.io)
* Implementation in HTML/CSS/JS by Timendus - 2024 (https://github.com/Timendus)
*
* See https://github.com/librespeed/speedtest/issues/585
*/
@import url("colors.css");
@import url("fonts.css");
@import url("main.css");
@import url("server-selector.css");
@import url("button.css");
@import url("results.css");
@import url("dialog.css");
/* Setting up the basic structure */
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
min-height: 100vh;
width: 100vw;
}
html {
background-color: var(--background-backup-color);
background-image: url("../images/background.jpeg");
background-repeat: no-repeat;
background-position: center;
background-size: cover;
font-size: 10px;
@media screen and (max-width: 800px) {
font-size: 8px;
}
}
body {
font-family: "Inter", sans-serif;
background-color: var(--background-overlay-color);
color: var(--primary-text-color);
display: flex;
flex-direction: column;
}
/* Position the logo */
header {
padding: 4rem 7rem;
@media screen and (max-width: 800px) {
padding: 7rem 2rem;
text-align: center;
}
}
/* Position the source code link */
footer {
margin: auto auto 0 auto;
padding: 5rem;
& > p.source a {
font-size: 1.6rem;
font-weight: 700;
letter-spacing: -0.1rem;
color: var(--theme-green);
text-underline-offset: 0.3rem;
transition: text-underline-offset 0.2s;
&:hover {
color: var(--theme-pink);
text-underline-offset: 0.5rem;
}
}
@media screen and (max-width: 800px) {
padding: 4rem;
}
}
+58
View File
@@ -0,0 +1,58 @@
/* Texts on the front page */
main {
text-align: center;
padding: 0 2rem;
flex: 1;
& > h1 {
margin: 0.2rem;
font-size: 3.6rem;
font-weight: 400;
letter-spacing: -0.2rem;
color: var(--primary-text-color);
}
& p {
margin-top: 8rem;
font-size: 1.6rem;
line-height: 2.5rem;
font-weight: 400;
letter-spacing: -0.1rem;
color: var(--secondary-text-color);
&#privacy-warning {
min-height: 5.3rem;
& > span {
font-weight: 700;
color: var(--theme-green);
}
&.hidden {
opacity: 0;
pointer-events: none;
}
}
}
& > p.tagline {
margin-top: 0;
margin-bottom: 6rem;
font-size: 2rem;
color: var(--tagline-text-color);
}
& a {
font-size: 1.6rem;
font-weight: 700;
letter-spacing: -0.1rem;
color: var(--secondary-text-color);
text-underline-offset: 0.3rem;
transition: text-underline-offset 0.2s;
&:hover {
color: var(--theme-green);
text-underline-offset: 0.5rem;
}
}
}
+260
View File
@@ -0,0 +1,260 @@
/* Variables */
:root {
--gauge-width: 32rem;
--gauge-height: 22rem;
--progress-width: 0.6rem;
--speed-width: 3rem;
}
/* Layout for the gauges */
.gauge-layout {
display: flex;
flex-direction: row;
align-items: start;
justify-content: center;
gap: 5rem;
margin: 5rem auto 3rem auto;
@media screen and (max-width: 1100px) {
display: grid;
grid-template-areas:
"download upload"
"ping jitter";
justify-items: center;
justify-content: center;
--gauge-width: min(40vw, 32rem);
--gauge-height: min(28vw, 22rem);
--progress-width: min(1.2vw, 0.6rem);
--speed-width: min(4vw, 3rem);
}
@media screen and (max-width: 500px) {
gap: 5rem 2rem;
}
}
/* The download/upload speed gauges */
/**
* One thing I should really document here is the weird `transform: scale(1);`
* and `position: fixed` in this code. This is a nasty little trick to allow the
* gauge pointer to break out of the `overflow: hidden` of the .speed element.
* We need the `overflow: hidden` to hide the arc that's rotating into view when
* the value goes up. But we do want to see the full pointer, even when it's at
* zero. This degrades fairly gracefully into showing half of the pointer when
* browsers don't understand this.
*
* Trick taken from this article:
* https://medium.com/@thomas.ryu/css-overriding-the-parents-overflow-hidden-90c75a0e7296
*/
div.gauge {
position: relative;
transform: scale(1);
width: var(--gauge-width);
height: var(--gauge-height);
&.download {
grid-area: download;
}
&.upload {
grid-area: upload;
}
& > .progress,
& > .speed {
position: absolute;
top: 0;
left: 0;
width: var(--gauge-width);
height: calc(var(--gauge-width) / 2);
overflow: hidden;
&:after,
&:before {
content: "";
position: absolute;
box-sizing: border-box;
}
}
& > .progress {
&:before,
&:after {
top: 0;
left: 0;
width: var(--gauge-width);
height: calc(var(--gauge-width) / 2);
border-radius: 50% 50% 0 0 / 100% 100% 0 0;
border: var(--progress-width) solid var(--gauge-background-color);
border-bottom: 0;
transform-origin: bottom center;
transform: rotate(var(--progress-rotation));
transition: transform 0.2s linear;
}
&:after {
top: calc(var(--gauge-width) / 2);
border-radius: 0 0 50% 50% / 0 0 100% 100%;
border: var(--progress-width) solid var(--gauge-background-color);
border-top: 0;
transform-origin: top center;
}
}
& > .speed {
&:before,
&:after {
transform: rotate(var(--speed-rotation));
transition: transform 0.2s ease;
transition-timing-function: cubic-bezier(0.56, 0.04, 0.59, 0.91);
}
&:before {
position: fixed;
top: calc(var(--gauge-width) / 2 - var(--speed-width) / 3);
left: var(--progress-width);
width: 0;
height: 0;
border-top: calc(var(--speed-width) / 3) solid transparent;
border-bottom: calc(var(--speed-width) / 3) solid transparent;
border-right: calc(var(--speed-width) * 0.97) solid
var(--gauge-background-color);
z-index: 1;
transform-origin: calc(var(--gauge-width) / 2 - var(--progress-width))
calc(var(--speed-width) / 3);
}
&:after {
top: calc(var(--gauge-width) / 2);
left: calc(var(--progress-width) - 0.1rem);
width: calc(var(--gauge-width) - var(--progress-width) * 2 + 0.2rem);
height: calc(var(--gauge-width) / 2 - var(--progress-width) + 0.1rem);
border-radius: 0 0 50% 50% / 0 0 100% 100%;
border: var(--speed-width) solid var(--gauge-background-color);
border-top: 0;
transform-origin: top center;
}
}
&.enabled {
&.download {
& > .progress:after {
border-color: var(--theme-pink);
}
& > .speed {
&:before {
border-right-color: var(--gauge-pointer-pink);
}
&:after {
border-color: var(--theme-pink);
}
}
}
&.upload {
& > .progress:after {
border-color: var(--theme-green);
}
& > .speed {
&:before {
border-right-color: var(--gauge-pointer-green);
}
&:after {
border-color: var(--theme-green);
}
}
}
& > h1 > span {
color: var(--primary-text-color);
}
}
& > h1,
& > h2 {
display: block;
position: absolute;
width: 100%;
font-family: "Inter", sans-serif;
font-size: 2.1rem;
letter-spacing: -0.1rem;
color: var(--secondary-text-color);
}
& > h1 {
bottom: calc(var(--gauge-height) - var(--gauge-width) / 2);
font-weight: 300;
& > span {
font-size: 5.5rem;
font-weight: 200;
display: block;
color: var(--secondary-text-color);
letter-spacing: -0.3rem;
}
}
& > h2 {
bottom: 0;
font-weight: 700;
text-transform: uppercase;
}
@media screen and (max-width: 500px) {
& > h1 {
font-size: 3vw;
& > span {
font-size: 8vw;
}
}
& > h2 {
font-size: 3vw;
}
}
}
/* Styling for Ping and Jitter */
.ping,
.jitter {
grid-area: jitter;
display: flex;
align-items: end;
height: calc(var(--gauge-width) / 2);
width: 13rem;
font-size: 2.1rem;
letter-spacing: -0.1rem;
font-weight: 300;
color: var(--ping-and-jitter-secondary-text-color);
& > .label {
font-weight: 700;
}
& > .value {
color: var(--ping-and-jitter-primary-text-color);
}
&.hidden {
display: none;
}
@media screen and (max-width: 1100px) {
width: 100%;
height: auto;
justify-content: center !important;
}
@media screen and (max-width: 500px) {
font-size: 1.8rem;
}
}
.ping {
grid-area: ping;
justify-content: end;
}
+171
View File
@@ -0,0 +1,171 @@
/* The server selector fake dropdown */
.server-selector {
position: relative;
width: 50rem;
margin: 0rem auto;
display: none;
&.active {
display: block;
}
@media screen and (max-width: 500px) {
width: 100%;
}
& > .chosen {
position: relative;
height: 8.8rem;
border: 1px solid var(--server-selector-border-color);
border-radius: 0.8rem;
background-color: var(--server-selector-background-color);
cursor: pointer;
transition: border-color 0.2s;
&:hover {
border-color: var(--server-selector-hover-border-color);
}
& > div.chevron {
content: "";
position: absolute;
display: block;
width: 32px;
height: 32px;
right: 1.8rem;
top: 1rem;
}
& > p {
margin: 0;
position: absolute;
left: 2.4rem;
top: 1.5rem;
font-size: 1.6rem;
font-weight: 400;
letter-spacing: -0.1rem;
color: var(--theme-green);
}
& > h2 {
position: absolute;
left: 2.4rem;
right: 2.4rem;
bottom: 1rem;
font-size: 2.4rem;
font-weight: 700;
letter-spacing: -0.2rem;
color: var(--primary-text-color);
text-align: left;
text-transform: uppercase;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
& span {
font-weight: 400;
}
}
}
/* Special case for when we have only one server */
&.single-server {
& > .chosen {
cursor: default;
&:hover {
border-color: var(--server-selector-border-color);
}
& > div.chevron {
display: none;
}
}
}
/* Overrides for when the test is running and the selector is disabled */
&.disabled {
pointer-events: none;
& > .chosen {
cursor: default;
&:hover {
border-color: var(--server-selector-border-color);
}
& > p {
color: var(--secondary-text-disabled-color);
}
& > h2 {
color: var(--primary-text-disabled-color);
}
}
}
/* Styling for the list of servers that pops out */
& > ul.servers {
position: absolute;
width: 50rem;
max-height: 70vh;
overflow-y: auto;
z-index: 1;
border: 1px solid var(--server-selector-border-color);
border-radius: 0.8rem;
background-color: var(--server-selector-background-color);
list-style: none;
transform: scaleY(0);
transform-origin: top;
transition: transform 0.1s;
&.active {
transform: scaleY(1);
}
@media screen and (max-width: 800px) {
width: 100%;
}
& > li {
&:first-child a {
padding-top: 1.5rem;
}
&:last-child a {
padding-bottom: 1.5rem;
}
& a {
display: block;
padding: 0.7rem 2.4rem;
font-size: 2.4rem;
font-weight: 700;
letter-spacing: -0.2rem;
color: var(--sprint-text-color);
text-transform: uppercase;
text-decoration: none;
text-align: left;
cursor: pointer;
transition: background-color 0.2s;
& span {
font-weight: 400;
}
&:hover {
background-color: var(--server-selector-hover-background-color);
}
}
}
}
/* Styling for the sponsor text under the dropdown */
& > p.sponsor {
margin: 1rem 0 5rem 0;
& a {
font-weight: 400;
}
}
}
+120
View File
@@ -0,0 +1,120 @@
package web
import (
"net"
"net/http"
"regexp"
"strings"
)
// normalizeCandidateIP validates and normalizes an IP address candidate
// from a request header. It trims whitespace, takes the first comma-separated
// token (for XFF-like headers that may contain a chain), and validates.
func normalizeCandidateIP(raw string, ipv6 bool) string {
ip := strings.TrimSpace(raw)
// For XFF-like values, take the first address before a comma
if idx := strings.Index(ip, ","); idx != -1 {
ip = strings.TrimSpace(ip[:idx])
}
if ip == "" {
return ""
}
if ipv6 {
parsed := net.ParseIP(ip)
if parsed != nil && parsed.To16() != nil && parsed.To4() == nil {
return strings.TrimPrefix(ip, "::ffff:")
}
return ""
}
parsed := net.ParseIP(ip)
if parsed != nil {
return strings.TrimPrefix(ip, "::ffff:")
}
return ""
}
// getClientIP extracts the real client IP from the request using the following
// priority chain, mirroring the PHP getIP_util.php behavior:
//
// 1. CF-Connecting-IPv6 (Cloudflare, must be a valid IPv6)
// 2. Client-IP
// 3. X-Real-IP
// 4. X-Forwarded-For (first address in the chain)
// 5. RemoteAddr (fallback)
func getClientIP(r *http.Request) string {
// 1. Cloudflare IPv6 header — must be a valid IPv6 address
if cf := r.Header.Get("CF-Connecting-IPv6"); cf != "" {
if ip := normalizeCandidateIP(cf, true); ip != "" {
return strings.TrimPrefix(ip, "::ffff:")
}
}
// 24. Other forwarding / proxy headers — accept any valid IP
for _, header := range []string{"Client-IP", "X-Real-IP", "X-Forwarded-For"} {
if v := r.Header.Get(header); v != "" {
if ip := normalizeCandidateIP(v, false); ip != "" {
return strings.TrimPrefix(ip, "::ffff:")
}
}
}
// 5. Fallback: RemoteAddr set by the server
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
// RemoteAddr may not have a port in some environments
ip = r.RemoteAddr
}
if parsed := net.ParseIP(ip); parsed != nil {
return strings.TrimPrefix(ip, "::ffff:")
}
return ""
}
// classifyPrivateIP returns a human-readable description if the IP is a
// private or special-purpose address, or an empty string otherwise.
// Mirrors the PHP getLocalOrPrivateIpInfo() function.
func classifyPrivateIP(ip string) string {
// Strip IPv4-mapped IPv6 prefix if present
ip = strings.TrimPrefix(ip, "::ffff:")
switch {
case ip == "::1":
return "localhost IPv6 access"
case strings.HasPrefix(ip, "fe80:"):
return "link-local IPv6 access"
// ULA IPv6 (fc00::/7): fc00:: - fdff:ffff:...
case isULAIPv6(ip):
return "ULA IPv6 access"
case strings.HasPrefix(ip, "127."):
return "localhost IPv4 access"
case strings.HasPrefix(ip, "10."):
return "private IPv4 access"
case mustCompile(`^172\.(1[6-9]|2\d|3[01])\.`).MatchString(ip):
return "private IPv4 access"
case strings.HasPrefix(ip, "192.168"):
return "private IPv4 access"
case strings.HasPrefix(ip, "169.254"):
return "link-local IPv4 access"
case mustCompile(`^100\.([6-9][0-9]|1[0-2][0-7])\.`).MatchString(ip):
return "CGNAT IPv4 access"
}
return ""
}
// isULAIPv6 checks if an IP is a Unique Local IPv6 Unicast Address (fc00::/7).
func isULAIPv6(ipStr string) bool {
ip := net.ParseIP(ipStr)
if ip == nil || ip.To16() == nil {
return false
}
// fc00::/7 means the first 7 bits are 1111110
// So the first byte & 0xFE must equal 0xFC
return ip[0]&0xFE == 0xFC
}
// mustCompile is a helper that compiles a regex and panics on error
// (safe to use for static patterns).
func mustCompile(pattern string) *regexp.Regexp {
return regexp.MustCompile(pattern)
}
+141 -7
View File
@@ -5,15 +5,18 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net"
"net/http" "net/http"
"os"
"strconv" "strconv"
"strings" "strings"
"github.com/oschwald/maxminddb-golang"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/umahmood/haversine" "github.com/umahmood/haversine"
"github.com/librespeed/speedtest/config" "github.com/librespeed/speedtest-go/config"
"github.com/librespeed/speedtest/results" "github.com/librespeed/speedtest-go/results"
) )
var ( var (
@@ -68,7 +71,7 @@ func getIPInfo(addr string) results.IPInfoResponse {
} }
func SetServerLocation(conf *config.Config) { 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) log.Infof("Configured server coordinates: %.6f, %.6f", conf.ServerLat, conf.ServerLng)
serverCoord.Lat = conf.ServerLat serverCoord.Lat = conf.ServerLat
serverCoord.Lon = conf.ServerLng serverCoord.Lon = conf.ServerLng
@@ -140,16 +143,147 @@ func calculateDistance(clientLocation string, unit string) string {
} }
dist, km := haversine.Distance(clientCoord, serverCoord) dist, km := haversine.Distance(clientCoord, serverCoord)
unitString := " mi"
switch unit { switch unit {
case "km": case "km":
dist = km dist = km
unitString = " km" rounded := roundToNearest10(dist)
if dist < 20 {
return "<20 km"
}
return fmt.Sprintf("%.0f km", rounded)
case "NM": case "NM":
dist = km * 0.539957 dist = km * 0.539957
unitString = " NM" return fmt.Sprintf("%.2f NM", dist)
default: // miles
distMi := dist
rounded := roundToNearest10(distMi)
if distMi < 15 {
return "<15 mi"
}
return fmt.Sprintf("%.0f mi", rounded)
}
}
// roundToNearest10 rounds a float64 to the nearest 10, matching PHP round($d, -1)
func roundToNearest10(val float64) float64 {
return float64(int64(val/10+0.5)) * 10
}
// GeoIP database holder (lazily opened on first use)
var (
geoIPReader *maxminddb.Reader
geoIPOpened bool
)
// getGeoIPData looks up the given IP in the configured GeoIP .mmdb database
// and returns ISP and country information if available.
// It returns nil if GeoIP is not configured or the lookup fails.
func getGeoIPData(ipStr string) *struct {
ASName string
CountryName string
} {
conf := config.LoadedConfig()
if conf.GeoIPDatabaseFile == "" {
return nil
} }
return fmt.Sprintf("%.2f%s", dist, unitString) if !geoIPOpened {
geoIPOpened = true
if _, err := os.Stat(conf.GeoIPDatabaseFile); os.IsNotExist(err) {
log.Warnf("GeoIP database file not found: %s", conf.GeoIPDatabaseFile)
return nil
}
reader, err := maxminddb.Open(conf.GeoIPDatabaseFile)
if err != nil {
log.Warnf("Failed to open GeoIP database: %s", err)
return nil
}
geoIPReader = reader
}
if geoIPReader == nil {
return nil
}
ip := net.ParseIP(ipStr)
if ip == nil {
return nil
}
// Try ipinfo.io offline database format first
var ipinfoResult map[string]interface{}
if err := geoIPReader.Lookup(ip, &ipinfoResult); err != nil {
log.Warnf("GeoIP lookup failed: %s", err)
return nil
}
if len(ipinfoResult) == 0 {
return nil
}
result := &struct {
ASName string
CountryName string
}{}
// ipinfo.io offline format uses "as_name" and "country_name"
if v, ok := ipinfoResult["as_name"].(string); ok {
result.ASName = v
}
if v, ok := ipinfoResult["country_name"].(string); ok {
result.CountryName = v
}
// If ipinfo format fields are empty, try standard MaxMind GeoIP2 format
if result.ASName == "" {
// Try autonomous_system > organization
if as, ok := ipinfoResult["autonomous_system"].(map[string]interface{}); ok {
if v, ok := as["organization"].(string); ok {
result.ASName = v
}
}
}
if result.CountryName == "" {
if country, ok := ipinfoResult["country"].(map[string]interface{}); ok {
if v, ok := country["names"].(map[string]interface{}); ok {
if n, ok := v["en"].(string); ok {
result.CountryName = n
}
}
}
// Fallback: direct "country" string field (as used by some GeoIP DBs)
if result.CountryName == "" {
if v, ok := ipinfoResult["country"].(string); ok {
result.CountryName = v
}
}
}
if result.ASName == "" && result.CountryName == "" {
return nil
}
return result
}
// getISPInfoByPriority tries to fetch ISP info using the ipinfo.io API first,
// then falls back to the configured offline GeoIP database, mirroring PHP behavior.
func getISPInfoByPriority(addr string) results.IPInfoResponse {
// First try: ipinfo.io API
info := getIPInfo(addr)
if info.Organization != "" || info.Country != "" {
return info
}
// Second try: offline GeoIP database
geo := getGeoIPData(addr)
if geo != nil {
info.Organization = geo.ASName
info.Country = geo.CountryName
return info
}
// Fallback: empty result (will show IP only)
return info
} }
+42
View 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-go/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
View 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-go/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
}
+87 -66
View File
@@ -1,14 +1,16 @@
package web package web
import ( import (
"embed"
"encoding/json" "encoding/json"
"io" "io"
"io/fs"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
"os"
"regexp" "regexp"
"strconv" "strconv"
"strings"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
@@ -17,8 +19,8 @@ import (
"github.com/pires/go-proxyproto" "github.com/pires/go-proxyproto"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/librespeed/speedtest/config" "github.com/librespeed/speedtest-go/config"
"github.com/librespeed/speedtest/results" "github.com/librespeed/speedtest-go/results"
) )
const ( const (
@@ -26,6 +28,9 @@ const (
chunkSize = 1048576 chunkSize = 1048576
) )
//go:embed assets
var defaultAssets embed.FS
var ( var (
// generate random data for download test on start to minimize runtime overhead // generate random data for download test on start to minimize runtime overhead
randomData = getRandomData(chunkSize) randomData = getRandomData(chunkSize)
@@ -46,38 +51,53 @@ func ListenAndServe(conf *config.Config) error {
r.Use(middleware.NoCache) r.Use(middleware.NoCache)
r.Use(middleware.Recoverer) r.Use(middleware.Recoverer)
addr := net.JoinHostPort(conf.BindAddress, conf.Port) var assetFS http.FileSystem
log.Infof("Starting backend server on %s", addr) if fi, err := os.Stat(conf.AssetsPath); os.IsNotExist(err) || !fi.IsDir() {
r.Get("/*", pages) log.Warnf("Configured asset path %s does not exist or is not a directory, using default assets", conf.AssetsPath)
r.HandleFunc("/empty", empty) sub, err := fs.Sub(defaultAssets, "assets")
r.HandleFunc("/backend/empty", empty) if err != nil {
r.Get("/garbage", garbage) log.Fatalf("Failed when processing default assets: %s", err)
r.Get("/backend/garbage", garbage) }
r.Get("/getIP", getIP) assetFS = http.FS(sub)
r.Get("/backend/getIP", getIP) } else {
r.Get("/results", results.DrawPNG) assetFS = justFilesFilesystem{fs: http.Dir(conf.AssetsPath), readDirBatchSize: 2}
r.Get("/results/", results.DrawPNG) }
r.Get("/backend/results", results.DrawPNG)
r.Get("/backend/results/", results.DrawPNG) r.Get(conf.BaseURL+"/*", pages(assetFS, conf.BaseURL))
r.Post("/results/telemetry", results.Record) r.HandleFunc(conf.BaseURL+"/empty", empty)
r.Post("/backend/results/telemetry", results.Record) r.HandleFunc(conf.BaseURL+"/backend/empty", empty)
r.HandleFunc("/stats", results.Stats) r.Get(conf.BaseURL+"/garbage", garbage)
r.HandleFunc("/backend/stats", results.Stats) 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)
r.Get(conf.BaseURL+"/results/json", results.JSONResult)
r.Get(conf.BaseURL+"/backend/results/json", results.JSONResult)
// PHP frontend default values compatibility // PHP frontend default values compatibility
r.HandleFunc("/empty.php", empty) r.HandleFunc(conf.BaseURL+"/empty.php", empty)
r.HandleFunc("/backend/empty.php", empty) r.HandleFunc(conf.BaseURL+"/backend/empty.php", empty)
r.Get("/garbage.php", garbage) r.Get(conf.BaseURL+"/garbage.php", garbage)
r.Get("/backend/garbage.php", garbage) r.Get(conf.BaseURL+"/backend/garbage.php", garbage)
r.Get("/getIP.php", getIP) r.Get(conf.BaseURL+"/getIP.php", getIP)
r.Get("/backend/getIP.php", getIP) r.Get(conf.BaseURL+"/backend/getIP.php", getIP)
r.Post("/results/telemetry.php", results.Record) r.Post(conf.BaseURL+"/results/telemetry.php", results.Record)
r.Post("/backend/results/telemetry.php", results.Record) r.Post(conf.BaseURL+"/backend/results/telemetry.php", results.Record)
r.HandleFunc("/stats.php", results.Stats) r.HandleFunc(conf.BaseURL+"/stats.php", results.Stats)
r.HandleFunc("/backend/stats.php", results.Stats) r.HandleFunc(conf.BaseURL+"/backend/stats.php", results.Stats)
r.Get(conf.BaseURL+"/results/json.php", results.JSONResult)
r.Get(conf.BaseURL+"/backend/results/json.php", results.JSONResult)
go listenProxyProtocol(conf, r) go listenProxyProtocol(conf, r)
return http.ListenAndServe(addr, r)
return startListener(conf, r)
} }
func listenProxyProtocol(conf *config.Config, r *chi.Mux) { func listenProxyProtocol(conf *config.Config, r *chi.Mux) {
@@ -96,14 +116,33 @@ func listenProxyProtocol(conf *config.Config, r *chi.Mux) {
} }
} }
func pages(w http.ResponseWriter, r *http.Request) { func pages(fs http.FileSystem, BaseURL string) http.HandlerFunc {
if r.RequestURI == "/" { var removeBaseURL *regexp.Regexp
r.RequestURI = "/index.html" 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() return fn
fs := justFilesFilesystem{fs: http.Dir(conf.AssetsPath), readDirBatchSize: 2} }
http.FileServer(fs).ServeHTTP(w, r)
// sendPHPCORSHeaders sets CORS headers matching the PHP backend's ?cors parameter behavior.
// This is for API compatibility with the PHP version; the global CORS middleware already handles CORS.
func sendPHPCORSHeaders(w http.ResponseWriter, r *http.Request) {
if r.FormValue("cors") == "true" {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST")
w.Header().Set("Access-Control-Allow-Headers", "Content-Encoding, Content-Type")
}
} }
func empty(w http.ResponseWriter, r *http.Request) { func empty(w http.ResponseWriter, r *http.Request) {
@@ -114,11 +153,13 @@ func empty(w http.ResponseWriter, r *http.Request) {
} }
_ = r.Body.Close() _ = r.Body.Close()
sendPHPCORSHeaders(w, r)
w.Header().Set("Connection", "keep-alive") w.Header().Set("Connection", "keep-alive")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
func garbage(w http.ResponseWriter, r *http.Request) { func garbage(w http.ResponseWriter, r *http.Request) {
sendPHPCORSHeaders(w, r)
w.Header().Set("Content-Description", "File Transfer") w.Header().Set("Content-Description", "File Transfer")
w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment; filename=random.dat") w.Header().Set("Content-Disposition", "attachment; filename=random.dat")
@@ -154,37 +195,17 @@ func garbage(w http.ResponseWriter, r *http.Request) {
func getIP(w http.ResponseWriter, r *http.Request) { func getIP(w http.ResponseWriter, r *http.Request) {
var ret results.Result var ret results.Result
clientIP := r.RemoteAddr clientIP := getClientIP(r)
clientIP = strings.ReplaceAll(clientIP, "::ffff:", "")
ip, _, err := net.SplitHostPort(r.RemoteAddr) // Add anti-cache headers matching PHP behavior
if err == nil { w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0, s-maxage=0")
clientIP = ip w.Header().Add("Cache-Control", "post-check=0, pre-check=0")
} w.Header().Set("Pragma", "no-cache")
isSpecialIP := true sendPHPCORSHeaders(w, r)
switch {
case clientIP == "::1":
ret.ProcessedString = clientIP + " - localhost IPv6 access"
case strings.HasPrefix(clientIP, "fe80:"):
ret.ProcessedString = clientIP + " - link-local IPv6 access"
case strings.HasPrefix(clientIP, "127."):
ret.ProcessedString = clientIP + " - localhost IPv4 access"
case strings.HasPrefix(clientIP, "10."):
ret.ProcessedString = clientIP + " - private IPv4 access"
case regexp.MustCompile(`^172\.(1[6-9]|2\d|3[01])\.`).MatchString(clientIP):
ret.ProcessedString = clientIP + " - private IPv4 access"
case strings.HasPrefix(clientIP, "192.168"):
ret.ProcessedString = clientIP + " - private IPv4 access"
case strings.HasPrefix(clientIP, "169.254"):
ret.ProcessedString = clientIP + " - link-local IPv4 access"
case regexp.MustCompile(`^100\.([6-9][0-9]|1[0-2][0-7])\.`).MatchString(clientIP):
ret.ProcessedString = clientIP + " - CGNAT IPv4 access"
default:
isSpecialIP = false
}
if isSpecialIP { if desc := classifyPrivateIP(clientIP); desc != "" {
ret.ProcessedString = clientIP + " - " + desc
b, _ := json.Marshal(&ret) b, _ := json.Marshal(&ret)
if _, err := w.Write(b); err != nil { if _, err := w.Write(b); err != nil {
log.Errorf("Error writing to client: %s", err) log.Errorf("Error writing to client: %s", err)
@@ -198,7 +219,7 @@ func getIP(w http.ResponseWriter, r *http.Request) {
ret.ProcessedString = clientIP ret.ProcessedString = clientIP
if getISPInfo { if getISPInfo {
ispInfo := getIPInfo(clientIP) ispInfo := getISPInfoByPriority(clientIP)
ret.RawISPInfo = ispInfo ret.RawISPInfo = ispInfo
removeRegexp := regexp.MustCompile(`AS\d+\s`) removeRegexp := regexp.MustCompile(`AS\d+\s`)