├── .gitignore ├── LICENSE ├── README.md ├── speedtest.go └── speedtest ├── client.go ├── config.go ├── coordinates.go ├── download.go ├── latency.go ├── latency_test.go ├── opts.go ├── server.go ├── upload.go ├── upload_test.go └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 4 | *.o 5 | *.a 6 | *.so 7 | 8 | # Folders 9 | _obj 10 | _test 11 | 12 | # Architecture specific extensions/prefixes 13 | *.[568vq] 14 | [568vq].out 15 | 16 | *.cgo1.go 17 | *.cgo2.c 18 | _cgo_defun.c 19 | _cgo_gotypes.go 20 | _cgo_export.* 21 | 22 | _testmain.go 23 | 24 | *.exe 25 | *.test 26 | *.prof 27 | 28 | .idea 29 | *.iml 30 | speedtest_cli.py 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2019 Ruslan Lopatin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > This project is no longer maintained. Please consider using one of its [forks](https://github.com/surol/speedtest-cli/network/members) 2 | 3 | speedtest.net CLI 4 | ================= 5 | 6 | This is a simple command line client to speedtest.net written in Go. 7 | 8 | It is a direct port from https://github.com/sivel/speedtest-cli written in Python. It lacks some of the features though, e.g. `-mini` and `-share` options are not supported. 9 | 10 | Installation 11 | ------------ 12 | 13 | Run te following commands in console 14 | ``` 15 | go get github.com/surol/speedtest-cli 16 | go install github.com/surol/speedtest-cli 17 | ``` 18 | 19 | Usage 20 | ----- 21 | 22 | Without any arguments `speedtest-cli` tests the speed against the closest server with the lowest latency. 23 | 24 | The following command line options are available: 25 | ``` 26 | -bytes 27 | Display values in bytes instead of bits. Does not affect the image generated by -share 28 | -h Shorthand for -help option 29 | -help 30 | Show usage information and exit 31 | -interface string 32 | IP address of network interface to bind to 33 | -list 34 | Display a list of speedtest.net servers sorted by distance 35 | -quiet 36 | Suppress verbose output, only show basic information 37 | -secure 38 | Use HTTPS instead of HTTP when communicating with speedtest.net operated servers 39 | -server uint 40 | Specify a server ID to test against 41 | -timeout duration 42 | HTTP timeout duration. Default 10s (default 10s) 43 | -version 44 | Show the version number and exit 45 | ``` 46 | -------------------------------------------------------------------------------- /speedtest.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/surol/speedtest-cli/speedtest" 5 | "fmt" 6 | "os" 7 | "flag" 8 | "log" 9 | "time" 10 | ) 11 | 12 | func version() { 13 | fmt.Print(speedtest.Version) 14 | } 15 | 16 | func usage() { 17 | fmt.Fprint(os.Stderr, "Command line interface for testing internet bandwidth using speedtest.net.\n\n") 18 | flag.PrintDefaults() 19 | } 20 | 21 | func main() { 22 | opts := speedtest.ParseOpts() 23 | 24 | switch { 25 | case opts.Help: 26 | usage() 27 | return 28 | case opts.Version: 29 | version() 30 | return 31 | } 32 | 33 | client := speedtest.NewClient(opts) 34 | 35 | if opts.List { 36 | servers, err := client.AllServers() 37 | if err != nil { 38 | log.Fatalf("Failed to load server list: %v\n", err) 39 | } 40 | fmt.Println(servers) 41 | return 42 | } 43 | 44 | config, err := client.Config() 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | client.Log("Testing from %s (%s)...\n", config.Client.ISP, config.Client.IP) 50 | 51 | server := selectServer(opts, client); 52 | 53 | downloadSpeed := server.DownloadSpeed() 54 | reportSpeed(opts, "Download", downloadSpeed) 55 | 56 | uploadSpeed := server.UploadSpeed() 57 | reportSpeed(opts, "Upload", uploadSpeed) 58 | } 59 | 60 | func reportSpeed(opts *speedtest.Opts, prefix string, speed int) { 61 | if opts.SpeedInBytes { 62 | fmt.Printf("%s: %.2f MiB/s\n", prefix, float64(speed) / (1 << 20)) 63 | } else { 64 | fmt.Printf("%s: %.2f Mib/s\n", prefix, float64(speed) / (1 << 17)) 65 | } 66 | } 67 | 68 | func selectServer(opts *speedtest.Opts, client speedtest.Client) (selected *speedtest.Server) { 69 | if opts.Server != 0 { 70 | servers, err := client.AllServers() 71 | if err != nil { 72 | log.Fatal("Failed to load server list: %v\n", err) 73 | return nil 74 | } 75 | selected = servers.Find(opts.Server) 76 | if selected == nil { 77 | log.Fatalf("Server not found: %d\n", opts.Server) 78 | return nil 79 | } 80 | selected.MeasureLatency(speedtest.DefaultLatencyMeasureTimes, speedtest.DefaultErrorLatency) 81 | } else { 82 | servers, err := client.ClosestServers() 83 | if err != nil { 84 | log.Fatal("Failed to load server list: %v\n", err) 85 | return nil 86 | } 87 | selected = servers.MeasureLatencies( 88 | speedtest.DefaultLatencyMeasureTimes, 89 | speedtest.DefaultErrorLatency).First() 90 | } 91 | 92 | if opts.Quiet { 93 | log.Printf("Ping: %d ms\n", selected.Latency / time.Millisecond) 94 | } else { 95 | client.Log("Hosted by %s (%s) [%.2f km]: %d ms\n", 96 | selected.Sponsor, 97 | selected.Name, 98 | selected.Distance, 99 | selected.Latency / time.Millisecond) 100 | } 101 | 102 | return selected 103 | } 104 | -------------------------------------------------------------------------------- /speedtest/client.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "net/http" 5 | "fmt" 6 | "runtime" 7 | "strings" 8 | "io" 9 | "net" 10 | "log" 11 | "encoding/xml" 12 | "io/ioutil" 13 | "sync" 14 | ) 15 | 16 | type Client interface { 17 | Log(format string, a ...interface{}) 18 | Config() (*Config, error) 19 | LoadConfig(ret chan ConfigRef) 20 | NewRequest(method string, url string, body io.Reader) (*http.Request, error) 21 | Get(url string) (resp *Response, err error) 22 | Post(url string, bodyType string, body io.Reader) (resp *Response, err error) 23 | AllServers() (*Servers, error) 24 | LoadAllServers(ret chan ServersRef) 25 | ClosestServers() (*Servers, error) 26 | LoadClosestServers(ret chan ServersRef) 27 | } 28 | 29 | type client struct { 30 | http.Client 31 | opts *Opts 32 | mutex sync.Mutex 33 | config chan ConfigRef 34 | allServers chan ServersRef 35 | closestServers chan ServersRef 36 | } 37 | 38 | type Response http.Response 39 | 40 | func NewClient(opts *Opts) Client { 41 | dialer := &net.Dialer{ 42 | Timeout: opts.Timeout, 43 | KeepAlive: opts.Timeout, 44 | } 45 | 46 | if len(opts.Interface) != 0 { 47 | dialer.LocalAddr = &net.IPAddr{IP: net.ParseIP(opts.Interface)} 48 | if dialer.LocalAddr == nil { 49 | log.Fatalf("Invalid source IP: %s\n", opts.Interface) 50 | } 51 | } 52 | 53 | transport := &http.Transport{ 54 | Proxy: http.ProxyFromEnvironment, 55 | Dial: dialer.Dial, 56 | TLSHandshakeTimeout: opts.Timeout, 57 | ExpectContinueTimeout: opts.Timeout, 58 | } 59 | 60 | client := &client{ 61 | Client: http.Client{ 62 | Transport: transport, 63 | Timeout: opts.Timeout, 64 | }, 65 | opts: opts, 66 | } 67 | 68 | return client; 69 | } 70 | 71 | func (client *client) NewRequest(method string, url string, body io.Reader) (*http.Request, error) { 72 | if strings.HasPrefix(url, ":") { 73 | if client.opts.Secure { 74 | url = "https" + url 75 | } else { 76 | url = "http" + url 77 | } 78 | } 79 | req, err := http.NewRequest(method, url, body); 80 | if err == nil { 81 | req.Header.Set( 82 | "User-Agent", 83 | "Mozilla/5.0 " + 84 | fmt.Sprintf("(%s; U; %s; en-us)", runtime.GOOS, runtime.GOARCH) + 85 | fmt.Sprintf("Go/%s", runtime.Version()) + 86 | fmt.Sprintf("(KHTML, like Gecko) speedtest-cli/%s", Version)) 87 | } 88 | return req, err; 89 | } 90 | 91 | func (client *client) Get(url string) (resp *Response, err error) { 92 | req, err := client.NewRequest("GET", url, nil); 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | htResp, err := client.Client.Do(req) 98 | 99 | return (*Response)(htResp), err; 100 | } 101 | 102 | func (client *client) Post(url string, bodyType string, body io.Reader) (resp *Response, err error) { 103 | req, err := client.NewRequest("POST", url, body) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | req.Header.Set("Content-Type", bodyType) 109 | htResp, err := client.Client.Do(req) 110 | 111 | return (*Response)(htResp), err; 112 | } 113 | 114 | func (resp *Response) ReadContent() ([]byte, error) { 115 | content, err := ioutil.ReadAll(resp.Body) 116 | cerr := resp.Body.Close() 117 | if err != nil { 118 | return nil, err; 119 | } 120 | if cerr != nil { 121 | return content, cerr; 122 | } 123 | return content, nil; 124 | } 125 | 126 | func (resp *Response) ReadXML(out interface{}) error { 127 | content, err := resp.ReadContent() 128 | if err != nil { 129 | return err; 130 | } 131 | return xml.Unmarshal(content, out) 132 | } 133 | -------------------------------------------------------------------------------- /speedtest/config.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "encoding/xml" 5 | "strings" 6 | "strconv" 7 | "log" 8 | ) 9 | 10 | type ClientConfig struct { 11 | Coordinates 12 | IP string `xml:"ip,attr"` 13 | ISP string `xml:"isp,attr"` 14 | ISPRating float32 `xml:"isprating,attr"` 15 | ISPDownloadAverage uint32 `xml:"ispdlavg,attr"` 16 | ISPUploadAverage uint32 `xml:"ispulavg,attr"` 17 | Rating float32 `xml:"rating,attr"` 18 | LoggedIn uint8 `xml:"loggedin,attr"` 19 | } 20 | 21 | type ConfigTime struct { 22 | Upload uint32 23 | Download uint32 24 | } 25 | 26 | type ConfigTimes []ConfigTime 27 | 28 | type Config struct { 29 | Client ClientConfig `xml:"client"` 30 | Times ConfigTimes `xml:"times"` 31 | } 32 | 33 | func (client *client) Log(format string, a ...interface{}) { 34 | if !client.opts.Quiet { 35 | log.Printf(format, a...) 36 | } 37 | } 38 | 39 | type ConfigRef struct { 40 | Config *Config 41 | Error error 42 | } 43 | 44 | func (client *client) Config() (*Config, error) { 45 | configChan := make(chan ConfigRef) 46 | client.LoadConfig(configChan) 47 | configRef := <-configChan 48 | return configRef.Config, configRef.Error 49 | } 50 | 51 | func (client *client) LoadConfig(ret chan ConfigRef) { 52 | client.mutex.Lock() 53 | defer client.mutex.Unlock() 54 | 55 | if client.config == nil { 56 | client.config = make(chan ConfigRef) 57 | go client.loadConfig() 58 | } 59 | 60 | go func() { 61 | result := <-client.config 62 | ret <- result 63 | client.config <- result 64 | }() 65 | } 66 | 67 | func (client *client) loadConfig() { 68 | client.Log("Retrieving speedtest.net configuration...") 69 | 70 | result := ConfigRef{} 71 | 72 | resp, err := client.Get("://www.speedtest.net/speedtest-config.php") 73 | if err != nil { 74 | result.Error = err 75 | } else { 76 | config := &Config{} 77 | err = resp.ReadXML(config) 78 | if err != nil { 79 | result.Error = err 80 | } else { 81 | result.Config = config 82 | } 83 | } 84 | 85 | client.config <- result 86 | } 87 | 88 | func (times ConfigTimes) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { 89 | for _, attr := range start.Attr { 90 | name := attr.Name.Local 91 | if dl := strings.HasPrefix(name, "dl"); dl || strings.HasPrefix(name, "ul") { 92 | num, err := strconv.Atoi(name[2:]) 93 | if err != nil { 94 | return err; 95 | } 96 | if num > cap(times) { 97 | newTimes := make([]ConfigTime, num) 98 | copy(newTimes, times) 99 | times = newTimes[0:num] 100 | } 101 | 102 | speed, err := strconv.ParseUint(attr.Value, 10, 32); 103 | 104 | if err != nil { 105 | return err 106 | } 107 | if dl { 108 | times[num - 1].Download = uint32(speed) 109 | } else { 110 | times[num - 1].Upload = uint32(speed) 111 | } 112 | } 113 | } 114 | 115 | return d.Skip() 116 | } 117 | 118 | -------------------------------------------------------------------------------- /speedtest/coordinates.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import "math" 4 | 5 | type Coordinates struct { 6 | Latitude float32 `xml:"lat,attr"` 7 | Longitude float32 `xml:"lon,attr"` 8 | } 9 | 10 | const radius = 6371 // km 11 | 12 | func radians32(degrees float32) float64 { 13 | return radians(float64(degrees)) 14 | } 15 | 16 | func radians(degrees float64) float64 { 17 | return degrees * math.Pi / 180 18 | } 19 | 20 | func (org Coordinates) DistanceTo(dest Coordinates) float64 { 21 | dlat := radians32(dest.Latitude - org.Latitude) 22 | dlon := radians32(dest.Longitude - org.Longitude) 23 | a := (math.Sin(dlat / 2) * math.Sin(dlat / 2) + 24 | math.Cos(radians32(org.Latitude)) * 25 | math.Cos(radians32(dest.Latitude)) * math.Sin(dlon / 2) * 26 | math.Sin(dlon / 2)) 27 | c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1 - a)) 28 | d := radius * c 29 | 30 | return d 31 | } 32 | -------------------------------------------------------------------------------- /speedtest/download.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "time" 5 | "log" 6 | "io" 7 | "fmt" 8 | "os" 9 | ) 10 | 11 | const downloadStreamLimit = 6 12 | const maxDownloadDuration = 10 * time.Second 13 | const downloadBufferSize = 4096 14 | const downloadRepeats = 5 15 | 16 | var downloadImageSizes = []int{350, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000} 17 | 18 | func (client *client) downloadFile(url string, start time.Time, ret chan int) { 19 | totalRead := 0 20 | defer func() { 21 | ret <- totalRead 22 | }() 23 | 24 | if (time.Since(start) > maxDownloadDuration) { 25 | return; 26 | } 27 | if !client.opts.Quiet { 28 | os.Stdout.WriteString(".") 29 | os.Stdout.Sync() 30 | } 31 | 32 | resp, err := client.Get(url) 33 | if err != nil { 34 | log.Printf("[%s] Download failed: %v\n", url, err) 35 | return; 36 | } 37 | 38 | defer resp.Body.Close() 39 | 40 | buf := make([]byte, downloadBufferSize) 41 | for time.Since(start) <= maxDownloadDuration { 42 | read, err := resp.Body.Read(buf) 43 | totalRead += read 44 | if err != nil { 45 | if err != io.EOF { 46 | log.Printf("[%s] Download error: %v\n", url, err) 47 | } 48 | break 49 | } 50 | } 51 | } 52 | 53 | func (server *Server) DownloadSpeed() int { 54 | client := server.client.(*client) 55 | if !client.opts.Quiet { 56 | os.Stdout.WriteString("Testing download speed: ") 57 | os.Stdout.Sync() 58 | } 59 | 60 | starterChan := make(chan int, downloadStreamLimit) 61 | downloads := downloadRepeats * len(downloadImageSizes) 62 | resultChan := make(chan int, downloadStreamLimit) 63 | start := time.Now() 64 | 65 | go func() { 66 | for _, size := range downloadImageSizes { 67 | for i := 0; i < downloadRepeats; i++ { 68 | url := server.RelativeURL(fmt.Sprintf("random%dx%d.jpg", size, size)) 69 | starterChan <- 1 70 | go func() { 71 | client.downloadFile(url, start, resultChan) 72 | <-starterChan 73 | }() 74 | } 75 | } 76 | close(starterChan) 77 | }() 78 | 79 | var totalSize int64 = 0; 80 | 81 | for i := 0; i < downloads; i++ { 82 | totalSize += int64(<-resultChan) 83 | } 84 | 85 | if !client.opts.Quiet { 86 | os.Stdout.WriteString("\n") 87 | os.Stdout.Sync() 88 | } 89 | 90 | duration := time.Since(start); 91 | 92 | return int(totalSize * int64(time.Second) / int64(duration)) 93 | } 94 | -------------------------------------------------------------------------------- /speedtest/latency.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "time" 5 | "strings" 6 | "sort" 7 | ) 8 | 9 | const DefaultLatencyMeasureTimes = 4 10 | const DefaultErrorLatency = time.Hour 11 | 12 | // Measures latencies for each server. 13 | // Returns server list sorted by latencies. 14 | // This is synchronous operation, because multiple simultaneous requests may affect results. 15 | func (servers *Servers) MeasureLatencies(times uint, errorLatency time.Duration) *Servers { 16 | first := true 17 | for _, server := range servers.List { 18 | if first { 19 | first = false 20 | server.client.Log("Measuring server latencies...") 21 | } 22 | server.doMeasureLatency(times, errorLatency) 23 | } 24 | 25 | latencies := &serverLatencies{List: make([]*Server, servers.Len())} 26 | copy(latencies.List, servers.List) 27 | sort.Sort(latencies) 28 | 29 | return (*Servers)(latencies) 30 | } 31 | 32 | type serverLatencies Servers 33 | 34 | func (servers *serverLatencies) Len() int { 35 | return len(servers.List) 36 | } 37 | 38 | func (servers *serverLatencies) Less(i, j int) bool { 39 | return servers.List[i].Latency < servers.List[j].Latency 40 | } 41 | 42 | func (servers *serverLatencies) Swap(i, j int) { 43 | temp := servers.List[i] 44 | servers.List[i] = servers.List[j] 45 | servers.List[j] = temp; 46 | } 47 | 48 | func (server *Server) MeasureLatency(times uint, errorLatency time.Duration) time.Duration { 49 | server.client.Log("Measuring server latency...\n") 50 | return server.doMeasureLatency(times, errorLatency); 51 | } 52 | 53 | func (server *Server) doMeasureLatency(times uint, errorLatency time.Duration) time.Duration { 54 | 55 | var results time.Duration = 0 56 | var i uint 57 | 58 | for i = 0; i < times; i++ { 59 | results += server.measureLatency(errorLatency) 60 | } 61 | 62 | server.Latency = time.Duration(results / time.Duration(times)) 63 | 64 | return server.Latency 65 | } 66 | 67 | func (server *Server) measureLatency(errorLatency time.Duration) time.Duration { 68 | url := server.RelativeURL("latency.txt") 69 | start := time.Now() 70 | resp, err := server.client.Get(url) 71 | duration := time.Since(start); 72 | if resp != nil { 73 | url = resp.Request.URL.String() 74 | } 75 | if err != nil { 76 | server.client.Log("[%s] Failed to detect latency: %v\n", url, err) 77 | return errorLatency 78 | } 79 | if resp.StatusCode != 200 { 80 | server.client.Log("[%s] Invalid latency detection HTTP status: %d\n", url, resp.StatusCode) 81 | duration = errorLatency 82 | } 83 | content, err := resp.ReadContent() 84 | if err != nil { 85 | server.client.Log("[%s] Failed to read latency response: %v\n", url, err) 86 | duration = errorLatency 87 | } 88 | if !strings.HasPrefix(string(content), "test=test") { 89 | server.client.Log("[%s] Invalid latency response: %s\n", url, content) 90 | duration = errorLatency 91 | } 92 | return duration 93 | } 94 | -------------------------------------------------------------------------------- /speedtest/latency_test.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net/http" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func Test_measureLatency(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | client Client 15 | input time.Duration 16 | want time.Duration 17 | }{ 18 | { 19 | name: "Client.Get() error with DefaultErrorLatency input", 20 | client: &latencyErrorClient{}, 21 | input: DefaultErrorLatency, 22 | want: DefaultErrorLatency, 23 | }, 24 | { 25 | name: "Client.Get() error with 10 Second input", 26 | client: &latencyErrorClient{}, 27 | input: 10 * time.Second, 28 | want: 10 * time.Second, 29 | }, 30 | } 31 | 32 | for _, tc := range tests { 33 | tc := tc 34 | t.Run(tc.name, func(t *testing.T) { 35 | s := &Server{client: tc.client} 36 | if got, want := s.measureLatency(tc.input), tc.want; got != want { 37 | t.Fatalf("unexpected result:\n- want: %v\n- got: %v", 38 | want, got) 39 | } 40 | }) 41 | } 42 | } 43 | 44 | // latencyErrorClient is a client returns error from most of the methods. 45 | type latencyErrorClient struct{} 46 | 47 | func (c *latencyErrorClient) Log(_ string, _ ...interface{}) {} 48 | func (c *latencyErrorClient) Config() (*Config, error) { 49 | return nil, errors.New("Config()") 50 | } 51 | func (c *latencyErrorClient) LoadConfig(_ chan ConfigRef) {} 52 | func (c *latencyErrorClient) NewRequest(_ string, _ string, _ io.Reader) (*http.Request, error) { 53 | return nil, errors.New("NewRequest()") 54 | } 55 | func (c *latencyErrorClient) Get(_ string) (resp *Response, err error) { 56 | return nil, errors.New("Get()") 57 | } 58 | func (c *latencyErrorClient) Post(_ string, _ string, _ io.Reader) (*Response, error) { 59 | return nil, errors.New("Post()") 60 | } 61 | func (c *latencyErrorClient) AllServers() (*Servers, error) { 62 | return nil, errors.New("AllServers()") 63 | } 64 | func (c *latencyErrorClient) LoadAllServers(_ chan ServersRef) {} 65 | func (c *latencyErrorClient) ClosestServers() (*Servers, error) { 66 | return nil, errors.New("ClosestServers()") 67 | } 68 | func (c *latencyErrorClient) LoadClosestServers(_ chan ServersRef) {} 69 | -------------------------------------------------------------------------------- /speedtest/opts.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "flag" 5 | "time" 6 | ) 7 | 8 | type Opts struct { 9 | SpeedInBytes bool 10 | Quiet bool 11 | List bool 12 | Server ServerID 13 | Interface string 14 | Timeout time.Duration 15 | Secure bool 16 | Help bool 17 | Version bool 18 | } 19 | 20 | func ParseOpts() *Opts { 21 | opts := new(Opts) 22 | 23 | flag.BoolVar(&opts.SpeedInBytes, "bytes", false, 24 | "Display values in bytes instead of bits. Does not affect the image generated by -share") 25 | flag.BoolVar(&opts.Quiet, "quiet", false, "Suppress verbose output, only show basic information") 26 | flag.BoolVar(&opts.List, "list", false, "Display a list of speedtest.net servers sorted by distance") 27 | flag.Uint64Var((*uint64)(&opts.Server), "server", 0, "Specify a server ID to test against") 28 | flag.StringVar(&opts.Interface, "interface", "", "IP address of network interface to bind to") 29 | flag.DurationVar(&opts.Timeout, "timeout", 10 * time.Second, "HTTP timeout duration. Default 10s") 30 | flag.BoolVar(&opts.Secure, "secure", false, 31 | "Use HTTPS instead of HTTP when communicating with speedtest.net operated servers") 32 | flag.BoolVar(&opts.Help, "help", false, "Show usage information and exit") 33 | flag.BoolVar(&opts.Help, "h", false, "Shorthand for -help option") 34 | flag.BoolVar(&opts.Version, "version", false, "Show the version number and exit") 35 | 36 | flag.Parse(); 37 | 38 | return opts 39 | } 40 | -------------------------------------------------------------------------------- /speedtest/server.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "errors" 5 | "sort" 6 | "fmt" 7 | "time" 8 | "net/url" 9 | "log" 10 | ) 11 | 12 | type ServerID uint64 13 | 14 | type Server struct { 15 | Coordinates 16 | URL string `xml:"url,attr"` 17 | Name string `xml:"name,attr"` 18 | Country string `xml:"country,attr"` 19 | CC string `xml:"cc,attr"` 20 | Sponsor string `xml:"sponsor,attr"` 21 | ID ServerID `xml:"id,attr"` 22 | URL2 string `xml:"url2,attr"` 23 | Host string `xml:"host,attr"` 24 | client Client `xml:"-"` 25 | Distance float64 `xml:"-"` 26 | Latency time.Duration `xml:"-"` 27 | } 28 | 29 | func (s *Server) String() string { 30 | return fmt.Sprintf("%8d: %s (%s, %s) [%.2f km] %s", s.ID, s.Sponsor, s.Name, s.Country, s.Distance, s.URL) 31 | } 32 | 33 | func (s *Server) RelativeURL(local string) string { 34 | u, err := url.Parse(s.URL) 35 | if err != nil { 36 | log.Fatalf("[%s] Failed to parse server URL: %v\n", s.URL, err) 37 | return "" 38 | } 39 | localURL, err := url.Parse(local) 40 | if err != nil { 41 | log.Fatalf("Failed to parse local URL `%s`: %v\n", local, err); 42 | } 43 | return u.ResolveReference(localURL).String() 44 | } 45 | 46 | type Servers struct { 47 | List []*Server `xml:"servers>server"` 48 | } 49 | 50 | type ServersRef struct { 51 | Servers *Servers 52 | Error error 53 | } 54 | 55 | func (servers *Servers) First() *Server { 56 | if len(servers.List) == 0 { 57 | return nil 58 | } 59 | return servers.List[0] 60 | } 61 | 62 | func (servers *Servers) Find(id ServerID) *Server { 63 | for _, server := range servers.List { 64 | if server.ID == id { 65 | return server 66 | } 67 | } 68 | return nil; 69 | } 70 | 71 | func (servers *Servers) Len() int { 72 | return len(servers.List) 73 | } 74 | 75 | func (servers *Servers) Less(i, j int) bool { 76 | server1 := servers.List[i] 77 | server2 := servers.List[j] 78 | if server1.ID == server2.ID { 79 | return false; 80 | } 81 | if server1.Distance < server2.Distance { 82 | return true; 83 | } 84 | if server1.Distance > server2.Distance { 85 | return false 86 | } 87 | return server1.ID < server2.ID 88 | } 89 | 90 | func (servers *Servers) Swap(i, j int) { 91 | temp := servers.List[i] 92 | servers.List[i] = servers.List[j] 93 | servers.List[j] = temp; 94 | } 95 | 96 | func (servers *Servers) truncate(max int) *Servers { 97 | size := servers.Len() 98 | if size <= max { 99 | return servers; 100 | } 101 | return &Servers{servers.List[:max]} 102 | } 103 | 104 | func (servers *Servers) String() string { 105 | out := "" 106 | for _, server := range servers.List { 107 | out += server.String() + "\n" 108 | } 109 | return out 110 | } 111 | 112 | func (servers *Servers) append(other *Servers) *Servers { 113 | if servers == nil { 114 | return other 115 | } 116 | servers.List = append(servers.List, other.List...) 117 | return servers 118 | } 119 | 120 | func (servers *Servers) sort(client Client, config *Config) { 121 | for _, server := range servers.List { 122 | server.client = client; 123 | server.Distance = server.DistanceTo(config.Client.Coordinates) 124 | } 125 | sort.Sort(servers) 126 | } 127 | 128 | func (servers *Servers) deduplicate() { 129 | dedup := make([]*Server, 0, len(servers.List)); 130 | var prevId ServerID = 0; 131 | for _, server := range servers.List { 132 | if prevId != server.ID { 133 | prevId = server.ID 134 | dedup = append(dedup, server); 135 | } 136 | } 137 | servers.List = dedup 138 | } 139 | 140 | var serverURLs = [...]string{ 141 | "://www.speedtest.net/speedtest-servers-static.php", 142 | "://c.speedtest.net/speedtest-servers-static.php", 143 | "://www.speedtest.net/speedtest-servers.php", 144 | "://c.speedtest.net/speedtest-servers.php", 145 | } 146 | 147 | var NoServersError error = errors.New("No servers available") 148 | 149 | func (client *client) AllServers() (*Servers, error) { 150 | serversChan := make(chan ServersRef) 151 | client.LoadAllServers(serversChan) 152 | serversRef := <-serversChan 153 | return serversRef.Servers, serversRef.Error 154 | } 155 | 156 | func (client *client) LoadAllServers(ret chan ServersRef) { 157 | client.mutex.Lock() 158 | defer client.mutex.Unlock() 159 | 160 | if client.allServers == nil { 161 | client.allServers = make(chan ServersRef) 162 | go client.loadServers() 163 | } 164 | 165 | go func() { 166 | result := <-client.allServers 167 | ret <- result 168 | client.allServers <- result// Make it available again 169 | }() 170 | } 171 | 172 | func (client *client) loadServers() { 173 | configChan := make(chan ConfigRef) 174 | client.LoadConfig(configChan); 175 | 176 | client.Log("Retrieving speedtest.net server list...") 177 | 178 | serversChan := make(chan *Servers, len(serverURLs)) 179 | for _, url := range serverURLs { 180 | go client.loadServersFrom(url, serversChan) 181 | } 182 | 183 | var servers *Servers 184 | 185 | for range serverURLs { 186 | servers = servers.append(<-serversChan); 187 | } 188 | 189 | result := ServersRef{} 190 | 191 | if servers.Len() == 0 { 192 | result.Error = NoServersError 193 | } else { 194 | configRef := <-configChan 195 | if configRef.Error != nil { 196 | result.Error = configRef.Error 197 | } else { 198 | servers.sort(client, configRef.Config) 199 | servers.deduplicate() 200 | result.Servers = servers 201 | } 202 | } 203 | 204 | client.allServers <- result 205 | } 206 | 207 | func (client *client) loadServersFrom(url string, ret chan *Servers) { 208 | resp, err := client.Get(url) 209 | if resp != nil { 210 | url = resp.Request.URL.String() 211 | } 212 | if err != nil { 213 | client.Log("[%s] Failed to retrieve server list: %v", url, err) 214 | } 215 | 216 | servers := &Servers{} 217 | if err = resp.ReadXML(servers); err != nil { 218 | client.Log("[%s] Failed to read server list: %v", url, err) 219 | } 220 | ret <- servers 221 | } 222 | 223 | func (client *client) ClosestServers() (*Servers, error) { 224 | serversChan := make(chan ServersRef) 225 | client.LoadClosestServers(serversChan) 226 | serversRef := <-serversChan 227 | return serversRef.Servers, serversRef.Error 228 | } 229 | 230 | func (client *client) LoadClosestServers(ret chan ServersRef) { 231 | client.mutex.Lock() 232 | defer client.mutex.Unlock() 233 | 234 | if client.closestServers == nil { 235 | client.closestServers = make(chan ServersRef) 236 | go client.loadClosestServers() 237 | } 238 | 239 | go func() { 240 | result := <-client.closestServers 241 | ret <- result 242 | client.closestServers <- result// Make it available again 243 | }() 244 | } 245 | 246 | func (client *client) loadClosestServers() { 247 | serversChan := make(chan ServersRef) 248 | client.LoadAllServers(serversChan) 249 | serversRef := <-serversChan 250 | if serversRef.Error != nil { 251 | client.closestServers <- serversRef 252 | } else { 253 | client.closestServers <- ServersRef{serversRef.Servers.truncate(5), nil} 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /speedtest/upload.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "time" 5 | "os" 6 | "log" 7 | "io" 8 | "strings" 9 | "crypto/rand" 10 | ) 11 | 12 | const maxUploadDuration = maxDownloadDuration 13 | const uploadStreamLimit = downloadStreamLimit 14 | const uploadRepeats = downloadRepeats 15 | 16 | var uploadSizes []int 17 | 18 | func init() { 19 | 20 | var uploadSizeSizes = []int{int(1000 * 1000 / 4), int(1000 * 1000 / 2)} 21 | 22 | uploadSizes = make([]int, len(uploadSizeSizes) * 25) 23 | for _, size := range uploadSizeSizes { 24 | for i := 0; i < 25; i++ { 25 | uploadSizes[i] = size 26 | } 27 | } 28 | } 29 | 30 | const safeChars = "0123456789abcdefghijklmnopqrstuv" 31 | 32 | type safeReader struct { 33 | in io.Reader 34 | } 35 | 36 | func (r safeReader) Read(p []byte) (n int, err error) { 37 | n, err = r.in.Read(p) 38 | 39 | for i := 0; i < n; i++ { 40 | p[i] = safeChars[p[i] & 31] 41 | } 42 | 43 | return n, err 44 | } 45 | 46 | func (client *client) uploadFile(url string, start time.Time, size int, ret chan int) { 47 | totalWrote := 0 48 | defer func() { 49 | ret <- totalWrote 50 | }() 51 | 52 | if (time.Since(start) > maxUploadDuration) { 53 | return; 54 | } 55 | if !client.opts.Quiet { 56 | os.Stdout.WriteString(".") 57 | os.Stdout.Sync() 58 | } 59 | 60 | resp, err := client.Post( 61 | url, 62 | "application/x-www-form-urlencoded", 63 | io.MultiReader( 64 | strings.NewReader("content1="), 65 | io.LimitReader(&safeReader{rand.Reader}, int64(size - 9)))) 66 | if err != nil { 67 | log.Printf("[%s] Upload failed: %v\n", url, err) 68 | return; 69 | } 70 | 71 | totalWrote = size 72 | 73 | defer resp.Body.Close() 74 | } 75 | 76 | func (server *Server) UploadSpeed() int { 77 | client := server.client.(*client) 78 | if !client.opts.Quiet { 79 | os.Stdout.WriteString("Testing upload speed: ") 80 | os.Stdout.Sync() 81 | } 82 | 83 | starterChan := make(chan int, uploadStreamLimit) 84 | uploads := uploadRepeats * len(uploadSizes) 85 | resultChan := make(chan int, uploadStreamLimit) 86 | start := time.Now() 87 | 88 | go func() { 89 | for _, size := range uploadSizes { 90 | size := size // local copy to avoid the data race. 91 | for i := 0; i < uploadRepeats; i++ { 92 | url := server.URL 93 | starterChan <- 1 94 | go func() { 95 | client.uploadFile(url, start, size, resultChan) 96 | <-starterChan 97 | }() 98 | } 99 | } 100 | close(starterChan) 101 | }() 102 | 103 | var totalSize int64 = 0; 104 | 105 | for i := 0; i < uploads; i++ { 106 | totalSize += int64(<-resultChan) 107 | } 108 | 109 | if !client.opts.Quiet { 110 | os.Stdout.WriteString("\n") 111 | os.Stdout.Sync() 112 | } 113 | 114 | duration := time.Since(start); 115 | 116 | return int(totalSize * int64(time.Second) / int64(duration)) 117 | } 118 | -------------------------------------------------------------------------------- /speedtest/upload_test.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestUpload(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | opts Opts 12 | }{ 13 | { 14 | name: "default options", 15 | opts: Opts{}, 16 | }, 17 | { 18 | name: "quiet option", 19 | opts: Opts{Quiet: true}, 20 | }, 21 | } 22 | 23 | for _, tc := range tests { 24 | tc := tc 25 | t.Run(tc.name, func(t *testing.T) { 26 | // set timeout to avoid the longer tests. 27 | tc.opts.Timeout = 10 * time.Second 28 | c := NewClient(&tc.opts) 29 | if _, err := c.Config(); err != nil { 30 | t.Fatalf("unexpected config error: %v", err) 31 | } 32 | s, err := c.ClosestServers() 33 | if err != nil { 34 | t.Fatalf("unexpected server selection error: %v", err) 35 | } 36 | // pick the firstest server to test. 37 | upload := s.MeasureLatencies( 38 | DefaultLatencyMeasureTimes, 39 | DefaultErrorLatency, 40 | ).First().UploadSpeed() 41 | t.Logf("upload %d bps", upload) 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /speedtest/version.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | const Version = "1.0.0" 4 | --------------------------------------------------------------------------------