added vendor

This commit is contained in:
Stephen McQuay 2015-09-29 01:04:04 -07:00
commit 6662b1e058
9 changed files with 1090 additions and 0 deletions

7
vendor/github.com/kellydunn/golang-geo/MIT_LICENSE generated vendored Normal file
View File

@ -0,0 +1,7 @@
Copyright (c) 2013 Kelly Dunn
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

150
vendor/github.com/kellydunn/golang-geo/point.go generated vendored Normal file
View File

@ -0,0 +1,150 @@
package geo
import (
"bytes"
"encoding/json"
"fmt"
"log"
"math"
)
// Represents a Physical Point in geographic notation [lat, lng].
type Point struct {
lat float64
lng float64
}
const (
// According to Wikipedia, the Earth's radius is about 6,371km
EARTH_RADIUS = 6371
)
// Returns a new Point populated by the passed in latitude (lat) and longitude (lng) values.
func NewPoint(lat float64, lng float64) *Point {
return &Point{lat: lat, lng: lng}
}
// Returns Point p's latitude.
func (p *Point) Lat() float64 {
return p.lat
}
// Returns Point p's longitude.
func (p *Point) Lng() float64 {
return p.lng
}
// Returns a Point populated with the lat and lng coordinates
// by transposing the origin point the passed in distance (in kilometers)
// by the passed in compass bearing (in degrees).
// Original Implementation from: http://www.movable-type.co.uk/scripts/latlong.html
func (p *Point) PointAtDistanceAndBearing(dist float64, bearing float64) *Point {
dr := dist / EARTH_RADIUS
bearing = (bearing * (math.Pi / 180.0))
lat1 := (p.lat * (math.Pi / 180.0))
lng1 := (p.lng * (math.Pi / 180.0))
lat2_part1 := math.Sin(lat1) * math.Cos(dr)
lat2_part2 := math.Cos(lat1) * math.Sin(dr) * math.Cos(bearing)
lat2 := math.Asin(lat2_part1 + lat2_part2)
lng2_part1 := math.Sin(bearing) * math.Sin(dr) * math.Cos(lat1)
lng2_part2 := math.Cos(dr) - (math.Sin(lat1) * math.Sin(lat2))
lng2 := lng1 + math.Atan2(lng2_part1, lng2_part2)
lng2 = math.Mod((lng2+3*math.Pi), (2*math.Pi)) - math.Pi
lat2 = lat2 * (180.0 / math.Pi)
lng2 = lng2 * (180.0 / math.Pi)
return &Point{lat: lat2, lng: lng2}
}
// Calculates the Haversine distance between two points in kilometers.
// Original Implementation from: http://www.movable-type.co.uk/scripts/latlong.html
func (p *Point) GreatCircleDistance(p2 *Point) float64 {
dLat := (p2.lat - p.lat) * (math.Pi / 180.0)
dLon := (p2.lng - p.lng) * (math.Pi / 180.0)
lat1 := p.lat * (math.Pi / 180.0)
lat2 := p2.lat * (math.Pi / 180.0)
a1 := math.Sin(dLat/2) * math.Sin(dLat/2)
a2 := math.Sin(dLon/2) * math.Sin(dLon/2) * math.Cos(lat1) * math.Cos(lat2)
a := a1 + a2
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return EARTH_RADIUS * c
}
// Calculates the initial bearing (sometimes referred to as forward azimuth)
// Original Implementation from: http://www.movable-type.co.uk/scripts/latlong.html
func (p *Point) BearingTo(p2 *Point) float64 {
dLon := (p2.lng - p.lng) * math.Pi / 180.0
lat1 := p.lat * math.Pi / 180.0
lat2 := p2.lat * math.Pi / 180.0
y := math.Sin(dLon) * math.Cos(lat2)
x := math.Cos(lat1)*math.Sin(lat2) -
math.Sin(lat1)*math.Cos(lat2)*math.Cos(dLon)
brng := math.Atan2(y, x) * 180.0 / math.Pi
return brng
}
// Calculates the midpoint between 'this' point and the supplied point.
// Original implementation from http://www.movable-type.co.uk/scripts/latlong.html
func (p *Point) MidpointTo(p2 *Point) *Point {
lat1 := p.lat * math.Pi / 180.0
lat2 := p2.lat * math.Pi / 180.0
lon1 := p.lng * math.Pi / 180.0
dLon := (p2.lng - p.lng) * math.Pi / 180.0
bx := math.Cos(lat2) * math.Cos(dLon)
by := math.Cos(lat2) * math.Sin(dLon)
lat3Rad := math.Atan2(
math.Sin(lat1)+math.Sin(lat2),
math.Sqrt(math.Pow(math.Cos(lat1)+bx, 2)+math.Pow(by, 2)),
)
lon3Rad := lon1 + math.Atan2(by, math.Cos(lat1)+bx)
lat3 := lat3Rad * 180.0 / math.Pi
lon3 := lon3Rad * 180.0 / math.Pi
return NewPoint(lat3, lon3)
}
// Renders the current Point to valid JSON.
// Implements the json.Marshaller Interface.
func (p *Point) MarshalJSON() ([]byte, error) {
res := fmt.Sprintf(`{"lat":%v, "lng":%v}`, p.lat, p.lng)
return []byte(res), nil
}
// Decodes the current Point from a JSON body.
// Throws an error if the body of the point cannot be interpreted by the JSON body
func (p *Point) UnmarshalJSON(data []byte) error {
// TODO throw an error if there is an issue parsing the body.
dec := json.NewDecoder(bytes.NewReader(data))
var values map[string]float64
err := dec.Decode(&values)
if err != nil {
log.Print(err)
return err
}
*p = *NewPoint(values["lat"], values["lng"])
return nil
}

138
vendor/github.com/kellydunn/golang-geo/point_test.go generated vendored Normal file
View File

@ -0,0 +1,138 @@
package geo
import (
"encoding/json"
"fmt"
"log"
"testing"
)
// Tests that a call to NewPoint should return a pointer to a Point with the specified values assigned correctly.
func TestNewPoint(t *testing.T) {
p := NewPoint(40.5, 120.5)
if p == nil {
t.Error("Expected to get a pointer to a new point, but got nil instead.")
}
if p.lat != 40.5 {
t.Errorf("Expected to be able to specify 40.5 as the lat value of a new point, but got %f instead", p.lat)
}
if p.lng != 120.5 {
t.Errorf("Expected to be able to specify 120.5 as the lng value of a new point, but got %f instead", p.lng)
}
}
// Tests that calling GetLat() after creating a new point returns the expected lat value.
func TestLat(t *testing.T) {
p := NewPoint(40.5, 120.5)
lat := p.Lat()
if lat != 40.5 {
t.Error("Expected a call to GetLat() to return the same lat value as was set before, but got %f instead", lat)
}
}
// Tests that calling GetLng() after creating a new point returns the expected lng value.
func TestLng(t *testing.T) {
p := NewPoint(40.5, 120.5)
lng := p.Lng()
if lng != 120.5 {
t.Error("Expected a call to GetLng() to return the same lat value as was set before, but got %f instead", lng)
}
}
// Seems brittle :\
func TestGreatCircleDistance(t *testing.T) {
// Test that SEA and SFO are ~ 1091km apart, accurate to 100 meters.
sea := &Point{lat: 47.4489, lng: -122.3094}
sfo := &Point{lat: 37.6160933, lng: -122.3924223}
sfoToSea := 1093.379199082169
dist := sea.GreatCircleDistance(sfo)
if !(dist < (sfoToSea+0.1) && dist > (sfoToSea-0.1)) {
t.Error("Unnacceptable result.", dist)
}
}
func TestPointAtDistanceAndBearing(t *testing.T) {
sea := &Point{lat: 47.44745785, lng: -122.308065668024}
p := sea.PointAtDistanceAndBearing(1090.7, 180)
// Expected results of transposing point
// ~1091km at bearing of 180 degrees
resultLat := 37.638557
resultLng := -122.308066
withinLatBounds := p.lat < resultLat+0.001 && p.lat > resultLat-0.001
withinLngBounds := p.lng < resultLng+0.001 && p.lng > resultLng-0.001
if !(withinLatBounds && withinLngBounds) {
t.Error("Unnacceptable result.", fmt.Sprintf("[%f, %f]", p.lat, p.lng))
}
}
func TestBearingTo(t *testing.T) {
p1 := &Point{lat: 40.7486, lng: -73.9864}
p2 := &Point{lat: 0.0, lng: 0.0}
bearing := p1.BearingTo(p2)
// Expected bearing 60 degrees
resultBearing := 100.610833
withinBearingBounds := bearing < resultBearing+0.001 && bearing > resultBearing-0.001
if !withinBearingBounds {
t.Error("Unnacceptable result.", fmt.Sprintf("%f", bearing))
}
}
func TestMidpointTo(t *testing.T) {
p1 := &Point{lat: 52.205, lng: 0.119}
p2 := &Point{lat: 48.857, lng: 2.351}
p := p1.MidpointTo(p2)
// Expected midpoint 50.5363°N, 001.2746°E
resultLat := 50.53632
resultLng := 1.274614
withinLatBounds := p.lat < resultLat+0.001 && p.lat > resultLat-0.001
withinLngBounds := p.lng < resultLng+0.001 && p.lng > resultLng-0.001
if !(withinLatBounds && withinLngBounds) {
t.Error("Unnacceptable result.", fmt.Sprintf("[%f, %f]", p.lat, p.lng))
}
}
// Enures that a point can be marhalled into JSON
func TestMarshalJSON(t *testing.T) {
p := NewPoint(40.7486, -73.9864)
res, err := json.Marshal(p)
if err != nil {
log.Print(err)
t.Error("Should not encounter an error when attempting to Marshal a Point to JSON")
}
if string(res) != `{"lat":40.7486,"lng":-73.9864}` {
t.Error("Point should correctly Marshal to JSON")
}
}
// Enures that a point can be unmarhalled from JSON
func TestUnmarshalJSON(t *testing.T) {
data := []byte(`{"lat":40.7486,"lng":-73.9864}`)
p := &Point{}
err := p.UnmarshalJSON(data)
if err != nil {
t.Errorf("Should not encounter an error when attempting to Unmarshal a Point from JSON")
}
if p.lat != 40.7486 || p.lng != -73.9864 {
t.Errorf("Point has mismatched data after Unmarshalling from JSON")
}
}

22
vendor/github.com/traetox/speedtest/LICENSE generated vendored Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2014 traetox
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

42
vendor/github.com/traetox/speedtest/README.md generated vendored Normal file
View File

@ -0,0 +1,42 @@
speedtest
=========
Command line speedtest which interacts with the speedtest.net infrastructure
Example below:
$ /opt/mygo/bin/speedtest -s="Phoe"
1 Matching servers:
------- ---------------- ----------------- ------------------
ID Name Sponsor Distance (km)
------- ---------------- ----------------- ------------------
0 Phoenix, AZ Pavlov Media 1111.75
------- ---------------- ----------------- ------------------
Enter server ID for bandwidth test, or "quit" to exit
ID> 0
Latency: ██▁▁▃▁▁▁▃▁▃▃▅▃▁▃▅▃█▃ 80ms avg 80ms median 83ms max 79ms min
Download: 23.01 Mb/s
Upload: 2.95 Mb/s
$ /opt/mygo/bin/speedtest
Gathering server list and testing...
5 Closest responding servers:
------- -------------------- ------------------------------------- ------------------ -----------------
ID Name Sponsor Distance (km) Latency (ms)
------- -------------------- ------------------------------------- ------------------ -----------------
0 Idaho Falls, ID Microserv 7.65 125.885984ms
1 Idaho Falls, ID Syringa Networks 7.65 138.980347ms
2 Rexburg, ID Brigham Young University - Idaho 48.50 130.055828ms
3 Bozeman, MT Montana Opticom LLC 262.15 114.208524ms
4 Bozeman, MT Global Net 262.15 116.363797ms
------- -------------------- ------------------------------------- ------------------ -----------------
Enter server ID for bandwidth test, or "quit" to exit
ID> 0
Latency: █▇▃▂▂▃▂▃▂▃▂▃▅▁▃▃▂▃▃▂ 123ms avg 123ms median 128ms max 122ms min
Download: 9.73 Mb/s
Upload: 2.89 Mb/s
$

220
vendor/github.com/traetox/speedtest/main.go generated vendored Normal file
View File

@ -0,0 +1,220 @@
package main
import (
"errors"
"flag"
"fmt"
"io"
"os"
"strconv"
"strings"
"github.com/Bowery/prompt"
"github.com/bndr/gotabulate"
"github.com/joliv/spark"
stdn "github.com/traetox/speedtest/speedtestdotnet"
//stdn "./speedtestdotnet" //for testing
)
const (
tableFormat = "simple"
maxFailureCount = 3
initialTestCount = 5
basePingCount = 5
fullTestCount = 20
)
var (
speedtestDuration = flag.Int("t", 3, "Target duration for speedtests (in seconds)")
search = flag.String("s", "", "Server name substring to search candidate servers")
)
func init() {
flag.Parse()
if *speedtestDuration <= 0 {
fmt.Fprintf(os.Stderr, "Invalid test duration")
os.Exit(-1)
}
}
func main() {
cfg, err := stdn.GetConfig()
if err != nil {
fmt.Printf("ERROR: %v\n", err)
return
}
if len(cfg.Servers) <= 0 {
fmt.Printf("No acceptable servers found\n")
return
}
var headers []string
var data [][]string
var testServers []stdn.Testserver
if *search == "" {
fmt.Printf("Gathering server list and testing...\n")
if testServers, err = autoGetTestServers(cfg); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(-1)
}
fmt.Printf("%d Closest responding servers:\n", len(testServers))
for i := range testServers {
data = append(data, []string{fmt.Sprintf("%d", i),
testServers[i].Name, testServers[i].Sponsor,
fmt.Sprintf("%.02f", testServers[i].Distance),
fmt.Sprintf("%s", testServers[i].Latency)})
}
headers = []string{"ID", "Name", "Sponsor", "Distance (km)", "Latency (ms)"}
} else {
if testServers, err = getSearchServers(cfg, *search); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(-1)
}
headers = []string{"ID", "Name", "Sponsor", "Distance (km)"}
fmt.Printf("%d Matching servers:\n", len(testServers))
for i := range testServers {
data = append(data, []string{fmt.Sprintf("%d", i),
testServers[i].Name, testServers[i].Sponsor,
fmt.Sprintf("%.02f", testServers[i].Distance)})
}
}
t := gotabulate.Create(data)
t.SetHeaders(headers)
t.SetWrapStrings(false)
fmt.Printf("%s", t.Render(tableFormat))
fmt.Printf("Enter server ID for bandwidth test, or \"quit\" to exit\n")
for {
s, err := prompt.Basic("ID> ", true)
if err != nil {
fmt.Printf("input failure \"%v\"\n", err)
os.Exit(-1)
}
//be REALLY forgiving on exit logic
if strings.HasPrefix(strings.ToLower(s), "exit") {
os.Exit(0)
}
if strings.HasPrefix(strings.ToLower(s), "quit") {
os.Exit(0)
}
//try to convert the string to a number
id, err := strconv.ParseUint(s, 10, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "\"%s\" is not a valid id\n", s)
continue
}
if id > uint64(len(testServers)) {
fmt.Fprintf(os.Stderr, "No server with ID \"%d\" available\n", id)
continue
}
if err = fullTest(testServers[id]); err != nil {
if err == io.EOF {
fmt.Fprintf(os.Stderr, "Error, the remote server kicked us.\n")
fmt.Fprintf(os.Stderr, "Maximum request size may have changed\n")
} else {
fmt.Fprintf(os.Stderr, "Test failed with unknown error: %v\n", err)
}
os.Exit(-1)
} else {
break //we are done
}
}
}
func testLatency(server stdn.Testserver) error {
//perform a full latency test
durs, err := server.Ping(fullTestCount)
if err != nil {
return err
}
var avg, max, min uint64
var latencies []float64
for i := range durs {
ms := uint64(durs[i].Nanoseconds() / 1000000)
latencies = append(latencies, float64(ms))
avg += ms
if ms > max {
max = ms
}
if ms < min || min == 0 {
min = ms
}
}
avg = avg / uint64(len(durs))
median := durs[len(durs)/2].Nanoseconds() / 1000000
sparkline := spark.Line(latencies)
fmt.Printf("Latency: %s\t%dms avg\t%dms median\t%dms max\t%dms min\n", sparkline, avg, median, max, min)
return nil
}
func testDownstream(server stdn.Testserver) error {
bps, err := server.Downstream(*speedtestDuration)
if err != nil {
return err
}
fmt.Printf("Download: %s\n", stdn.HumanSpeed(bps))
return nil
}
func testUpstream(server stdn.Testserver) error {
bps, err := server.Upstream(*speedtestDuration)
if err != nil {
return err
}
fmt.Printf("Upload: %s\n", stdn.HumanSpeed(bps))
return nil
}
func fullTest(server stdn.Testserver) error {
if err := testLatency(server); err != nil {
return err
}
if err := testDownstream(server); err != nil {
return err
}
if err := testUpstream(server); err != nil {
return err
}
return nil
}
func autoGetTestServers(cfg *stdn.Config) ([]stdn.Testserver, error) {
//get the first 5 closest servers
testServers := []stdn.Testserver{}
failures := 0
for i := range cfg.Servers {
if failures >= maxFailureCount {
if len(testServers) > 0 {
return testServers, nil
}
return nil, fmt.Errorf("Failed to perform latency test on closest servers\n")
}
if len(testServers) >= initialTestCount {
return testServers, nil
}
//get a latency from the server, the last latency will also be store in the
//server structure
if _, err := cfg.Servers[i].MedianPing(basePingCount); err != nil {
failures++
continue
}
testServers = append(testServers, cfg.Servers[i])
}
return testServers, nil
}
func getSearchServers(cfg *stdn.Config, query string) ([]stdn.Testserver, error) {
//get the first 5 closest servers
testServers := []stdn.Testserver{}
for i := range cfg.Servers {
if strings.Contains(strings.ToLower(cfg.Servers[i].Name), strings.ToLower(query)) {
testServers = append(testServers, cfg.Servers[i])
}
}
if len(testServers) == 0 {
return nil, errors.New("no servers found")
}
return testServers, nil
}

View File

@ -0,0 +1,277 @@
package speedtestdotnet
import (
"errors"
"fmt"
"io"
"net"
"sort"
"strconv"
"strings"
"time"
)
const (
maxDownstreamTestCount = 4
maxTransferSize = 8 * 1024 * 1024
pingTimeout = time.Second * 5
speedTestTimeout = time.Second * 10
cmdTimeout = time.Second
latencyMaxTestCount = 60
dataBlockSize = 16 * 1024 //16KB
)
var (
errInvalidServerResponse = errors.New("Invalid server response")
errPingFailure = errors.New("Failed to complete ping test")
errDontBeADick = errors.New("requested ping count too high")
startBlockSize = uint64(4096) //4KB
dataBlock []byte
)
func init() {
base := []byte("ABCDEFGHIJ")
dataBlock = make([]byte, dataBlockSize)
for i := range dataBlock {
dataBlock[i] = base[i%len(base)]
}
}
type durations []time.Duration
func (ts *Testserver) ping(count int) ([]time.Duration, error) {
var errRet []time.Duration
if count > latencyMaxTestCount {
return errRet, errDontBeADick
}
//establish connection to the host
conn, err := net.DialTimeout("tcp", ts.Host, pingTimeout)
if err != nil {
return errRet, err
}
defer conn.Close()
durs := []time.Duration{}
buff := make([]byte, 256)
for i := 0; i < count; i++ {
t := time.Now()
fmt.Fprintf(conn, "PING %d\n", uint(t.UnixNano()/1000000))
conn.SetReadDeadline(time.Now().Add(pingTimeout))
n, err := conn.Read(buff)
if err != nil {
return errRet, err
}
conn.SetReadDeadline(time.Time{})
d := time.Since(t)
flds := strings.Fields(strings.TrimRight(string(buff[0:n]), "\n"))
if len(flds) != 2 {
return errRet, errInvalidServerResponse
}
if flds[0] != "PONG" {
return errRet, errInvalidServerResponse
}
if _, err = strconv.ParseInt(flds[1], 10, 64); err != nil {
return errRet, errInvalidServerResponse
}
durs = append(durs, d)
}
if len(durs) != count {
return errRet, errPingFailure
}
return durs, nil
}
//MedianPing runs a latency test against the server and stores the median latency
func (ts *Testserver) MedianPing(count int) (time.Duration, error) {
var errRet time.Duration
durs, err := ts.ping(count)
if err != nil {
return errRet, err
}
sort.Sort(durations(durs))
ts.Latency = durs[count/2]
return durs[count/2], nil
}
//Ping will run count number of latency tests and return the results of each
func (ts *Testserver) Ping(count int) ([]time.Duration, error) {
return ts.ping(count)
}
//throwBytes chucks bytes at the remote server then listens for a response
func throwBytes(conn io.ReadWriter, count uint64) error {
var writeBytes uint64
var b []byte
buff := make([]byte, 128)
for writeBytes < count {
if (count - writeBytes) >= uint64(len(dataBlock)) {
b = dataBlock
} else {
b = dataBlock[0:(count - writeBytes)]
}
n, err := conn.Write(b)
if err != nil {
return err
}
writeBytes += uint64(n)
}
//read the response
n, err := conn.Read(buff)
if err != nil {
return err
}
if n == 0 {
return fmt.Errorf("Failed to get OK on upload")
}
if !strings.HasPrefix(string(buff[0:n]), "OK ") {
return fmt.Errorf("Failed to get OK on upload")
}
return nil
}
//readBytes reads until we get a newline or an error
func readBytes(rdr io.Reader, count uint64) error {
var rBytes uint64
buff := make([]byte, 4096)
for rBytes < count {
n, err := rdr.Read(buff)
if err != nil {
return err
}
rBytes += uint64(n)
if n == 0 {
break
}
if buff[n-1] == '\n' {
break
}
}
if rBytes != count {
return fmt.Errorf("Failed entire read: %d != %d", rBytes, count)
}
return nil
}
//Upstream measures upstream bandwidth in bps
func (ts *Testserver) Upstream(duration int) (uint64, error) {
var currBps uint64
sz := startBlockSize
conn, err := net.DialTimeout("tcp", ts.Host, speedTestTimeout)
if err != nil {
return 0, err
}
targetTestDuration := time.Second * time.Duration(duration)
defer conn.Close()
//we repeat the tests until we have a test that lasts at least N seconds
for i := 0; i < maxDownstreamTestCount; i++ {
//request a download of size sz and set a deadline
if err = conn.SetWriteDeadline(time.Now().Add(cmdTimeout)); err != nil {
return 0, err
}
cmdStr := fmt.Sprintf("UPLOAD %d 0\n", sz)
if _, err := conn.Write([]byte(cmdStr)); err != nil {
return 0, err
}
if err = conn.SetWriteDeadline(time.Time{}); err != nil {
return 0, err
}
ts := time.Now() //set start time mark
if err = conn.SetWriteDeadline(time.Now().Add(speedTestTimeout)); err != nil {
return 0, err
}
if err := throwBytes(conn, sz-uint64(len(cmdStr))); err != nil {
return 0, err
}
if err = conn.SetReadDeadline(time.Time{}); err != nil {
return 0, err
}
//check if our test was a reasonable timespan
dur := time.Since(ts)
currBps = bps(sz, dur)
if dur.Nanoseconds() > targetTestDuration.Nanoseconds() || sz == maxTransferSize {
_, err = fmt.Fprintf(conn, "QUIT\n")
return bps(sz, dur), err
}
//test was too short, try again
sz = calcNextSize(sz, dur)
if sz > maxTransferSize {
sz = maxTransferSize
}
}
_, err = fmt.Fprintf(conn, "QUIT\n")
return currBps, err
}
//Downstream measures upstream bandwidth in bps
func (ts *Testserver) Downstream(duration int) (uint64, error) {
var currBps uint64
sz := startBlockSize
conn, err := net.DialTimeout("tcp", ts.Host, speedTestTimeout)
if err != nil {
return 0, err
}
defer conn.Close()
targetTestDuration := time.Second * time.Duration(duration)
//we repeat the tests until we have a test that lasts at least N seconds
for i := 0; i < maxDownstreamTestCount; i++ {
//request a download of size sz and set a deadline
if err = conn.SetWriteDeadline(time.Now().Add(cmdTimeout)); err != nil {
return 0, err
}
fmt.Fprintf(conn, "DOWNLOAD %d\n", sz)
if err = conn.SetWriteDeadline(time.Time{}); err != nil {
return 0, err
}
ts := time.Now() //set start time mark
if err = conn.SetReadDeadline(time.Now().Add(speedTestTimeout)); err != nil {
return 0, err
}
//read until we get a newline
if err = readBytes(conn, sz); err != nil {
return 0, err
}
if err = conn.SetReadDeadline(time.Time{}); err != nil {
return 0, err
}
//check if our test was a reasonable timespan
dur := time.Since(ts)
currBps = bps(sz, dur)
if dur.Nanoseconds() > targetTestDuration.Nanoseconds() || sz == maxTransferSize {
_, err = fmt.Fprintf(conn, "QUIT\n")
return bps(sz, dur), err
}
//test was too short, try again
sz = calcNextSize(sz, dur)
if sz > maxTransferSize {
sz = maxTransferSize
}
}
_, err = fmt.Fprintf(conn, "QUIT\n")
return currBps, err
}
//calcNextSize takes the current preformance metrics and
//attempts to calculate what the next size should be
func calcNextSize(b uint64, dur time.Duration) uint64 {
if b == 0 {
return startBlockSize
}
target := time.Second * 5
return (b * uint64(target.Nanoseconds())) / uint64(dur.Nanoseconds())
}
//take the byte count and duration and calcuate a bits per second
func bps(byteCount uint64, dur time.Duration) uint64 {
bits := byteCount * 8
return uint64((bits * 1000000000) / uint64(dur.Nanoseconds()))
}
func (d durations) Len() int { return len(d) }
func (d durations) Less(i, j int) bool { return d[i].Nanoseconds() < d[j].Nanoseconds() }
func (d durations) Swap(i, j int) { d[i], d[j] = d[j], d[i] }

View File

@ -0,0 +1,30 @@
package speedtestdotnet
import (
"fmt"
)
var (
bits = 8
kb = uint64(1024)
mb = 1024 * kb
gb = 1024 * mb
tb = 1024 * gb
pb = 1024 * tb
tooDamnFast = "Too fast to test"
)
func HumanSpeed(bps uint64) string {
if bps > pb {
return tooDamnFast
} else if bps > tb {
return fmt.Sprintf("%.02f Tb/s", float64(bps)/float64(tb))
} else if bps > gb {
return fmt.Sprintf("%.02f Gb/s", float64(bps)/float64(gb))
} else if bps > mb {
return fmt.Sprintf("%.02f Mb/s", float64(bps)/float64(mb))
} else if bps > kb {
return fmt.Sprintf("%.02f Kb/s", float64(bps)/float64(kb))
}
return fmt.Sprintf("%d bps", bps)
}

View File

@ -0,0 +1,204 @@
package speedtestdotnet
import (
"encoding/xml"
"errors"
"fmt"
"net"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/kellydunn/golang-geo"
)
const (
serversConfigUrl string = `http://www.speedtest.net/speedtest-servers-static.php`
clientConfigUrl string = `http://www.speedtest.net/speedtest-config.php`
getTimeout time.Duration = 2 * time.Second
)
type Testserver struct {
Name string
Sponsor string
Country string
Lat float64
Long float64
Distance float64 //distance from server in KM
URLs []string
Host string
Latency time.Duration //latency in ms
}
type testServerlist []Testserver
type Config struct {
LicenseKey string
IP net.IP
Lat float64
Long float64
ISP string
Servers []Testserver
}
type sconfig struct {
XMLName xml.Name `xml:"server-config"`
Threads int `xml:"threadcount,attr"`
IgnoreIDs string `xml:"ignoreids,attr"`
}
type cconfig struct {
XMLName xml.Name `xml:"client"`
Ip string `xml:"ip,attr"`
Lat float64 `xml:"lat,attr"`
Long float64 `xml:"lon,attr"`
ISP string `xml:"isp,attr"`
ISPUpAvg uint `xml:"ispulavg,attr"`
ISPDlAvg uint `xml:"ispdlavg,attr"`
}
type speedtestConfig struct {
XMLName xml.Name `xml:"settings"`
License string `xml:"licensekey"`
ClientConfig cconfig `xml:"client"`
ServerConfig sconfig `xml:"server-config"`
}
type server struct {
XMLName xml.Name `xml:"server"`
Url string `xml:"url,attr"`
Url2 string `xml:"url2,attr"`
Lat float64 `xml:"lat,attr"`
Long float64 `xml:"lon,attr"`
Name string `xml:"name,attr"`
Country string `xml:"country,attr"`
CC string `xml:"cc,attr"`
Sponsor string `xml:"sponsor,attr"`
ID uint `xml:"id,attr"`
Host string `xml:"host,attr"`
}
type settings struct {
XMLName xml.Name `xml:"settings"`
Servers []server `xml:"servers>server"`
}
//GetServerList returns a list of servers in the native speedtest.net structure
func GetServerList() ([]server, error) {
//get a list of servers
clnt := http.Client{
Timeout: getTimeout,
}
//get the server configs
req, err := http.NewRequest("GET", serversConfigUrl, nil)
if err != nil {
return nil, err
}
resp, err := clnt.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Invalid status %s", resp.StatusCode)
}
xmlDec := xml.NewDecoder(resp.Body)
sts := settings{}
if err := xmlDec.Decode(&sts); err != nil {
return nil, err
}
return sts.Servers, nil
}
//GetConfig returns a configuration containing information about our client and a list of acceptable servers sorted by distance
func GetConfig() (*Config, error) {
//get a client configuration
clnt := http.Client{
Timeout: getTimeout,
}
//get the server configs
req, err := http.NewRequest("GET", clientConfigUrl, nil)
if err != nil {
return nil, err
}
resp, err := clnt.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Invalid status %s", resp.StatusCode)
}
xmlDec := xml.NewDecoder(resp.Body)
cc := speedtestConfig{}
if err := xmlDec.Decode(&cc); err != nil {
return nil, err
}
cfg := Config{
LicenseKey: cc.License,
IP: net.ParseIP(cc.ClientConfig.Ip),
Lat: cc.ClientConfig.Lat,
Long: cc.ClientConfig.Long,
ISP: cc.ClientConfig.ISP,
}
ignoreIDs := make(map[uint]bool, 1)
strIDs := strings.Split(cc.ServerConfig.IgnoreIDs, ",")
for i := range strIDs {
x, err := strconv.ParseUint(strIDs[i], 10, 32)
if err != nil {
continue
}
ignoreIDs[uint(x)] = false
}
srvs, err := GetServerList()
if err != nil {
return nil, err
}
if err := populateServers(&cfg, srvs, ignoreIDs); err != nil {
return nil, err
}
return &cfg, nil
}
func populateServers(cfg *Config, srvs []server, ignore map[uint]bool) error {
for i := range srvs {
//checking if we are ignoring this server
_, ok := ignore[srvs[i].ID]
if ok {
continue
}
srv := Testserver{
Name: srvs[i].Name,
Sponsor: srvs[i].Sponsor,
Country: srvs[i].Country,
Lat: srvs[i].Lat,
Long: srvs[i].Long,
Host: srvs[i].Host,
}
if srvs[i].Url != "" {
srv.URLs = append(srv.URLs, srvs[i].Url)
}
if srvs[i].Url2 != "" {
srv.URLs = append(srv.URLs, srvs[i].Url2)
}
p := geo.NewPoint(cfg.Lat, cfg.Long)
if p == nil {
return errors.New("Invalid client lat/long")
}
sp := geo.NewPoint(srvs[i].Lat, srvs[i].Long)
if sp == nil {
return errors.New("Invalid server lat/long")
}
srv.Distance = p.GreatCircleDistance(sp)
cfg.Servers = append(cfg.Servers, srv)
}
sort.Sort(testServerlist(cfg.Servers))
return nil
}
func (tsl testServerlist) Len() int { return len(tsl) }
func (tsl testServerlist) Swap(i, j int) { tsl[i], tsl[j] = tsl[j], tsl[i] }
func (tsl testServerlist) Less(i, j int) bool { return tsl[i].Distance < tsl[j].Distance }