File Coverage

speedtest
Criterion Covered Total %
statement 69 376 18.3
branch 3 192 1.5
condition 0 60 0.0
subroutine 20 30 66.6
pod n/a
total 92 658 13.9


line stmt bran cond sub pod time code
1             #!/pro/bin/perl
2              
3             # speedtest - test network speed using speedtest.net
4             # (m)'26 [2026-01-09] Copyright H.M.Brand 2014-2026
5              
6 4         680994 require 5.010;
7 4     4   17896 use strict;
  4         6  
  4         160  
8 4     4   18 use warnings;
  4         6  
  4         1063  
9              
10 4         19 our $VERSION = "0.31";
11 4         17 our $CMD = $0; $CMD =~ s{.*/}{};
  4         28  
12              
13             sub usage {
14 1 50   1   3 my $err = shift and select STDERR;
15 1         5 (my $p = $0) =~ s{.*/}{};
16 1         6 print <<"EOH";
17             usage: $p [ --no-geo | --country=NL ] [ --list | --ping[=n] ] [ options ]
18             --geo use Geo location (default true) for closest testserver
19             --all include *all* servers (default only in own country)
20             -c --country=IS use ISO country code for closest test server
21             --list-cc list country codes and countries with server count
22             -1 --one-line show summary in one line
23             -C --csv output in CSV (stamp,id,ping,tests,direction,speed,min,max)
24             --csv-eol-unix EOL = NL (default = CR NL) implies -C
25             -P --prtg output in XML for PRTG
26              
27             -l --list list test servers in chosen country sorted by distance
28             -p --ping[=40] list test servers in chosen country sorted by latency
29             --url show server url in list
30              
31             -s --server=nnn use testserver with id nnn
32             --server=file use testserver from file
33             -t --timeout=nnn set server timeout to nnn seconds
34             --url=sss use specific server url (do not scan) ext php
35             --mini=sss use specific server url (do not scan) ext from sss
36             --download test download speed (default true)
37             --upload test upload speed (default true)
38             -q --quick[=20] do a quick test (only the fastest 20 tests)
39             -Q --realquick do a real quick test (only the fastest 10 tests)
40             -T --try[=5] try all tests on the n fastest servers
41             -U --skip-undef skip results with no actual measurements
42              
43             -v --verbose[=1] set verbosity
44             --simple alias for -v0
45             --ip show IP for server
46             -V --version show version and exit
47             -? --help show this help
48             --man show the builtin manual (requires nroff)
49             --info show the builtin manual as plain text
50              
51             $p --list
52             $p --ping --country=BE
53             $p
54             $p -s 4358
55             $p --url=http://ookla.extraip.net
56             $p -q --no-download
57             $p -Q --no-upload
58              
59             EOH
60 1         0 exit $err;
61             } # usage
62              
63 4     4   2818 use Getopt::Long qw(:config bundling noignorecase);
  4         56881  
  4         29  
64 4         12 my $opt_c = "";
65 4         9 my $opt_v = 1;
66 4         43 my $opt_d = 1;
67 4         6 my $opt_u = 1;
68 4         31 my $opt_g = 1;
69 4         7 my $opt_q = 0;
70 4         8 my $opt_T = 1;
71 4         19 my $unit = [ 1, "bit" ];
72             GetOptions (
73 1     1   1869 "help|h|?" => sub { usage (0); },
74 1     1   1932 "V|version!" => sub { print "$CMD [$VERSION]\n"; exit 0; },
  1         0  
75             "v|verbose:2" => \$opt_v,
76 0     0   0 "simple!" => sub { $opt_v = 0; },
77 1     1   3596 "man" => sub { pod_nroff (); },
78 1     1   3344 "info" => sub { pod_text (); },
79              
80             "all!" => \my $opt_a,
81             "g|geo!" => \$opt_g,
82             "c|cc|country=s" => \$opt_c,
83             "list-cc!" => \my $opt_cc,
84             "1|one-line!" => \my $opt_1,
85             "C|csv!" => \my $opt_C,
86             "csv-eol-unix|".
87             "csv-eol-nl!" => \my $opt_CNL,
88             "P|prtg!" => \my $opt_P,
89              
90             "l|list!" => \my $list,
91             "p|ping:40" => \my $opt_ping,
92             "url:s" => \my $url,
93             "ip!" => \my $ip,
94              
95 0     0   0 "B|bytes" => sub { $unit = [ 8, "byte" ] },
96              
97 4 0       130 "T|try:5" => \$opt_T,
98             "s|server=s" => \my @server,
99             "t|timeout=i" => \my $timeout,
100             "d|download!" => \$opt_d,
101             "u|upload!" => \$opt_u,
102             "q|quick|fast:20" => \$opt_q,
103             "Q|realquick:10" => \$opt_q,
104             "U|skip-undef!" => \my $opt_U,
105              
106             "m|mini=s" => \my $mini,
107             "source=s" => \my $source, # NYI
108             ) or usage (1);
109              
110 0 0       0 $opt_CNL and $opt_C++;
111 0 0 0     0 $opt_C || $opt_P and $opt_v = 0;
112              
113 4     4   4408 use LWP::UserAgent;
  4         215256  
  4         194  
114 4     4   3346 use XML::Simple; # Can safely be replaced with XML::LibXML::Simple
  4         40380  
  4         26  
115 4     4   3462 use HTML::TreeBuilder;
  4         132897  
  4         48  
116 4     4   3261 use Term::ANSIColor;
  4         42467  
  4         506  
117 4     4   43 use Time::HiRes qw( gettimeofday tv_interval );
  4         12  
  4         121  
118 4     4   469 use List::Util qw( first sum );
  4         7  
  4         712  
119 4     4   2684 use Socket qw( inet_ntoa );
  4         19487  
  4         921  
120 4     4   2257 use Math::Trig;
  4         63548  
  4         1807  
121              
122             sub pod_text {
123 2     2   1421 require Pod::Text::Color;
124 2 50       114070 my $m = $ENV{NO_COLOR} ? "Pod::Text" : "Pod::Text::Color";
125 2         11 my $p = $m->new ();
126 2         346 open my $fh, ">", \my $out;
127 2         9 $p->parse_from_file ($0, $fh);
128 2         246379 close $fh;
129 2         76 print $out;
130 2         0 exit 0;
131             } # pod_text
132              
133             sub pod_nroff {
134 9 50   9   351 first { -x "$_/nroff" } grep { -d } split m/:+/ => $ENV{PATH} or pod_text ();
  1     1   21  
  9         127  
135              
136 0           require Pod::Man;
137 0           my $p = Pod::Man->new ();
138 0           open my $fh, "|-", "nroff", "-man";
139 0           $p->parse_from_file ($0, $fh);
140 0           close $fh;
141 0           exit 0;
142             } # pod_nroff
143              
144             # Debugging. Prefer Data::Peek over Data::Dumper if available
145 0     4   0 { use Data::Dumper;
  4         2803  
  4         39509  
  4         37961  
146 0         0 my $dp = eval { require Data::Peek; 1; };
  0         0  
  0         0  
147             sub ddumper {
148 0 0   0     $dp ? Data::Peek::DDumper (@_)
149             : print STDERR Dumper (@_);
150             } # ddumper
151             }
152              
153 0   0     0 $timeout ||= 10;
  0         0  
154 0         0 my $ua = LWP::UserAgent->new (
155             max_redirect => 2,
156             agent => "speedtest/$VERSION",
157             parse_head => 0,
158             timeout => $timeout,
159             cookie_jar => {},
160             );
161 0         0 $ua->env_proxy;
162              
163 0         0 binmode STDOUT, ":encoding(utf-8)";
164              
165             # Speedtest.net defines Mbit/s and kbit/s using 1000 as multiplier,
166             # https://support.speedtest.net/entries/21057567-What-do-mbps-and-kbps-mean-
167 0         0 my $k = 1000;
168              
169 0         0 my $config = get_config ();
170 0 0       0 my $client = $config->{"client"} or die "Config saw no client\n";
171 0 0       0 my $times = $config->{"times"} or die "Config saw no times\n";
172 0 0       0 my $downld = $config->{"download"} or die "Config saw no download\n";
173 0 0       0 my $upld = $config->{"upload"} or die "Config saw no upload\n";
174 0 0       0 $opt_v > 3 and ddumper {
175             client => $client,
176             times => $times,
177             down => $downld,
178             up => $upld,
179             };
180              
181 0 0 0     0 if ($url || $mini) {
182 0         0 $opt_g = 0;
183 0         0 $opt_c = "";
184 0         0 @server = ();
185 0         0 my $ping = 0.05;
186 0         0 my $name = "";
187 0         0 my $sponsor = "CLI";
188 0 0       0 if ($mini) {
189 0         0 my $t0 = [ gettimeofday ];
190 0         0 my $rsp = $ua->request (HTTP::Request->new (GET => $mini));
191 0         0 $ping = tv_interval ($t0);
192 0 0       0 $rsp->is_success or die $rsp->status_line . "\n";
193 0         0 my $tree = HTML::TreeBuilder->new ();
194 0 0       0 $tree->parse_content ($rsp->content) or die "Cannot parse\n";
195 0         0 my $ext = "";
196 0         0 for ($tree->look_down (_tag => "script")) {
197 0 0       0 my $c = ($_->content)[0] or next;
198 0 0 0     0 ref $c eq "ARRAY" && $c->[0] &&
      0        
199             $c->[0] =~ m{\b (?: upload_? | config ) Extension
200             \s*: \s* "? ([^"\s]+) }xi or next;
201 0         0 $ext = $1;
202 0         0 last;
203             }
204 0 0       0 $ext or die "No ext found\n";
205 0         0 ($url = $mini) =~ s{/*$}{/speedtest/upload.$ext};
206 0         0 $sponsor = $_->as_text for $tree->look_down (_tag => "title");
207 0   0     0 $name ||= $_->as_text for $tree->look_down (_tag => "h1");
208 0   0     0 $name ||= "Speedtest mini";
209             }
210             else {
211 0         0 $name = "Local";
212 0 0       0 $url =~ m{/\w+\.\w+$} or $url =~ s{/?$}{/speedtest/upload.php};
213 0         0 my $t0 = [ gettimeofday ];
214 0         0 my $rsp = $ua->request (HTTP::Request->new (GET => $url));
215 0         0 $ping = tv_interval ($t0);
216 0 0       0 $rsp->is_success or die $rsp->status_line . "\n";
217             }
218 0         0 (my $host = $url) =~ s{^\w+://([^/]+)(?:/.*)?}{$1};
219 0         0 $url = {
220             cc => "",
221             country => "",
222             dist => "0.0",
223             host => $host,
224             id => 0,
225             lat => "0.0000",
226             lon => "0.0000",
227             name => $name,
228             ping => $ping * 1000,
229             sponsor => $sponsor,
230             url => $url,
231             url2 => $url,
232             };
233             }
234              
235 0 0       0 if (@server) {
236 0         0 $opt_c = "";
237 0         0 $opt_a = 1;
238 0 0       0 unless ($server[0] =~ m{^[0-9]+$}) {
239 0 0       0 open my $fh, "<", $server[0] or die;#usage (1);
240 0         0 my $data = do { local $/; <$fh>; };
  0         0  
  0         0  
241 0         0 print $data;
242 0 0 0     0 $data =~ m/^\s*\{\s*(['"]?)cc\1\s*=>\s*(["'])[A-Z]{1,3}\2\s*,/ &&
243             $data =~ m/\s(["']?)id\1\s*=>\s*[0-9]+\s*,/ or die;#usage (1);
244 0         0 $data = eval $data;
245             $data->{dist} = distance ($client->{lat}, $client->{lon},
246 0         0 $data->{lat}, $data->{lon});
247 0         0 ($data->{url0} = $data->{url}) =~ s{/speedtest/upload.*}{};
248 0         0 $url = $data;
249             }
250             }
251             else {
252 0 0       0 if ($opt_c) {
    0          
253 0         0 $opt_c = uc $opt_c;
254             }
255             elsif ($opt_g) { # Try GeoIP
256 0 0       0 $opt_v > 5 and say STDERR "Testing Geo location";
257 0         0 my $url = "http://www.geoiptool.com";
258 0         0 my $rsp = $ua->request (HTTP::Request->new (GET => $url));
259 0 0       0 if ($rsp->is_success) {
260 0         0 my $tree = HTML::TreeBuilder->new ();
261 0 0       0 if ($tree->parse_content ($rsp->content)) {
262 0         0 foreach my $e ($tree->look_down (_tag => "div", class => "data-item")) {
263 0 0       0 $opt_v > 2 and say STDERR $e->as_text;
264 0 0       0 $e->as_text =~ m{Country code(?:\s*:)?\s*([A-Za-z]+)}i or next;
265 0         0 $opt_c = uc $1;
266 0         0 last;
267             }
268             }
269             }
270 0 0       0 unless ($opt_c) { # GEO-Ip failed :/
271 0 0       0 $opt_v and warn "GEO-IP failed. Getting country code based on nearest server\n";
272 0         0 my $keep_a = $opt_a;
273 0         0 $opt_a = 1;
274 0         0 my %list = servers ();
275 0         0 my $nearest = { dist => 9999999 };
276 0         0 foreach my $id (keys %list) {
277 0 0       0 $list{$id}{dist} < $nearest->{dist} and $nearest = $list{$id};
278             }
279 0 0       0 $opt_v > 3 and ddumper { nearest => $nearest };
280 0         0 $opt_c = $nearest->{cc};
281 0         0 $opt_a = $keep_a;
282             }
283             }
284 0   0     0 $opt_c ||= "IS"; # Iceland seems like a nice default :P
285             }
286              
287 0 0       0 if ($opt_cc) {
288 0         0 my %sl = get_servers ();
289 0         0 my %cc;
290 0         0 foreach my $s (values %sl) {
291 0         0 my $cc = $s->{cc};
292 0   0     0 $cc{$cc} //= { cc => $cc, country => $s->{country}, count => 0 };
293 0         0 $cc = $cc{$cc};
294 0         0 $cc->{count}++;
295             }
296 0         0 for (sort { $a->{cc} cmp $b->{cc} } values %cc) {
  0         0  
297 0         0 printf "%2s %-32s %4d\n", $_->{cc}, $_->{country}, $_->{count};
298             }
299 0         0 exit 0;
300             }
301              
302 0 0       0 if ($list) {
303 0         0 my %list = servers ();
304 0         0 my @fld = qw( id sponsor name dist );
305 0         0 my $fmt = "%3d: %5d - %-30.30s %-15.15s %7.2f km\n";
306 0 0       0 if (defined $url) {
307 0         0 push @fld, "url0";
308 0         0 $fmt .= " %s\n";
309             }
310 0         0 my $idx = 1;
311 0         0 printf $fmt, $idx++, @{$list{$_}}{@fld}
312 0         0 for sort { $list{$a}{dist} <=> $list{$b}{dist} } keys %list;
  0         0  
313 0         0 exit 0;
314             }
315              
316 0 0       0 if ($opt_ping) {
317 0         0 my @fld = qw( id sponsor name dist ping );
318 0         0 my $fmt = "%3d: %5d - %-30.30s %-15.15s %7.2f km %7.0f ms\n";
319 0 0       0 if (defined $url) {
320 0         0 push @fld, "url0";
321 0         0 $fmt .= " %s\n";
322             }
323 0         0 my $idx = 1;
324 0         0 printf $fmt, $idx++, @{$_}{@fld} for servers_by_ping ();
  0         0  
325 0         0 exit 0;
326             }
327              
328 0 0       0 $opt_v and say STDERR "Testing for $client->{ip} : $client->{isp} ($opt_c)";
329 0 0       0 $opt_P and print qq{<?xml version="1.0" encoding="UTF-8" ?>\n<prtg>\n},
330             qq{ <text>Testing from $client->{isp} ($client->{ip})</text>\n};
331              
332             # default action is to run on fastest server
333 0 0       0 my @srvrs = $url ? ($url) : servers_by_ping ();
334 0         0 my @hosts = grep { $_->{ping} < 1000 } @srvrs;
  0         0  
335 0 0       0 @server and $opt_T = @server;
336 0 0       0 @hosts > $opt_T and splice @hosts, $opt_T;
337 0         0 my @try;
338 0         0 foreach my $host (@hosts) {
339 0         0 $host->{sponsor} =~ s/\s+$//;
340 0 0       0 if ($opt_P) {
    0          
341 0         0 printf do { join "\n", map { " $_" }
  0         0  
342             "<result>",
343             " <channel>Ping</channel>",
344             " <customUnit>ms</customUnit>",
345             " <float>1</float>",
346             " <value>%0.2f</value>",
347             " </result>\n",
348 0         0 }, $host->{ping};
349             }
350             elsif ($opt_v) {
351 0         0 my $s = "";
352 0 0       0 if ($ip) {
353 0         0 (my $h = $host->{url}) =~ s{^\w+://([^/]+)(?:/.*)?$}{$1};
354 0         0 my @ad = gethostbyname ($h);
355 0         0 $s = join " " => "", map { inet_ntoa ($_) } @ad[4 .. $#ad];
  0         0  
356             }
357 0 0       0 @hosts > 1 and print STDERR "\n";
358             printf STDERR "Using %5d: %6.2f km %7.0f ms%s %s\n",
359 0         0 $host->{id}, $host->{dist}, $host->{ping}, $s, $host->{sponsor};
360             }
361 0 0       0 $opt_v > 3 and ddumper $host;
362 0         0 (my $base = $host->{url}) =~ s{/[^/]+$}{};
363              
364 0         0 my $dl = "-";
365 0 0       0 if ($opt_d) {
366 0 0       0 $opt_v and print STDERR "Test download ";
367             # http://ookla.extraip.net/speedtest/random350x350.jpg
368 0         0 my @url = @{$host->{dl_list} // [
369 0   0     0 map { ("$base/random${_}x${_}.jpg") x 4 }
  0         0  
370             350, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000 ]};
371 0         0 my @rslt;
372 0 0       0 $opt_q and splice @url, $opt_q;
373 0         0 foreach my $url (@url) {
374 0         0 my $req = HTTP::Request->new (GET => $url);
375 0         0 my $t0 = [ gettimeofday ];
376 0         0 my $rsp = $ua->request ($req);
377 0         0 my $elapsed = tv_interval ($t0);
378 0 0       0 unless ($rsp->is_success) {
379 0         0 warn "$url: ", $rsp->status_line, "\n";
380 0         0 next;
381             }
382 0         0 my $sz = length $rsp->content;
383 0         0 my $speed = 8 * $sz / $elapsed / $k / $k;
384 0         0 push @rslt, [ $sz, $elapsed, $speed ];
385 0 0       0 $opt_v and print STDERR ".";
386 0 0       0 $opt_v > 2 and printf STDERR "\n%12.3f %s (%7d) ", $speed, $url, $sz;
387             }
388 0         0 $dl = result ("Download", $host, scalar @url, @rslt);
389             }
390              
391 0         0 my $ul = "-";
392 0 0       0 if ($opt_u) {
393 0 0       0 $opt_v and print STDERR "Test upload ";
394 0         0 my @data = (0 .. 9, "a" .. "Z", "a" .. "z"); # Random pure ASCII data
395 0         0 my $data = join "" => map { $data[int rand $#data] } 0 .. 4192;
  0         0  
396 0         0 $data = "content1=".($data x 8192); # Total length just over 4 Mb
397 0         0 my @rslt;
398 0         0 my $url = $host->{url}; # .php, .asp, .aspx, .jsp
399             # see $upld->{mintestsize} and $upld->{maxchunksize} ?
400 0         0 my @size = map { $_ * 1000 }
  0         0  
401             # ((256) x 10, (512) x 10, (1024) x 10, (4096) x 10);
402             ((256) x 10, (512) x 10, (1024) x 5, (2048) x 5, (4096) x 5, (8192) x 5);
403 0 0       0 $opt_q and splice @size, $opt_q;
404 0         0 foreach my $sz (@size) {
405 0         0 my $req = HTTP::Request->new (POST => $url);
406 0         0 $req->content (substr $data, 0, $sz);
407 0         0 my $t0 = [ gettimeofday ];
408 0         0 my $rsp = $ua->request ($req);
409 0         0 my $elapsed = tv_interval ($t0);
410 0 0       0 unless ($rsp->is_success) {
411 0         0 warn "$url: ", $rsp->status_line, "\n";
412 0         0 next;
413             }
414 0         0 my $speed = 8 * $sz / $elapsed / $k / $k;
415 0         0 push @rslt, [ $sz, $elapsed, $speed ];
416 0 0       0 $opt_v and print STDERR ".";
417 0 0       0 $opt_v > 2 and printf STDERR "\n%12.3f %s (%7d) ", $speed, $url, $sz;
418             }
419              
420 0         0 $ul = result ("Upload", $host, scalar @size, @rslt);
421             }
422 0 0       0 my $sum = $dl eq "-" ? 0 : $dl;
423 0 0       0 $sum += $ul eq "-" ? 0 : $ul;
424 0   0     0 $sum ||= "-";
425 0         0 push @try => [ $host, $dl, $ul, $sum ];
426 0 0       0 $opt_1 and print "DL: $dl M$unit->[1]/s, UL: $ul M$unit->[1]/s, SRV: $host->{id}\n";
427             }
428 0 0       0 $opt_P and print " </prtg>\n";
429              
430 0 0 0     0 if ($opt_T and @try > 1) {
431 0         0 print "\n";
432 0         0 my $rank = 1;
433 0         0 foreach my $t (sort { $b->[-1] <=> $a->[-1] } @try) {
  0         0  
434 0         0 my ($host, $dl, $ul) = @$t;
435             printf "Rank %02d: Server: %6d %6.2f km %7.0f ms, DL: %s UL: %s\n",
436 0         0 $rank++, $host->{id}, $host->{dist}, $host->{ping}, $dl, $ul;
437             }
438             }
439              
440             sub result {
441 0     0     my ($dir, $host, $n, @rslt) = @_;
442              
443 0   0       my $size = (sum map { $_->[0] } @rslt) // 0;
  0            
444 0   0       my $time = (sum map { $_->[1] } @rslt) // 0;
  0            
445              
446 0           my @speed = sort { $a <=> $b } grep { $_ } map { $_->[2] } @rslt;
  0            
  0            
  0            
447 0 0 0       $opt_U && @speed == 0 and return;
448              
449 0   0       my $slow = $speed[ 0] // 0.000;
450 0   0       my $fast = $speed[-1] // 999999999.999;
451              
452 0   0       my $sp = sprintf "%8.3f", 8 * ($size / ($time || 1)) / $k / $k / $unit->[0];
453 0 0         if ($opt_C) {
    0          
454 0           my @d = localtime;
455             # stamp,id,ping,tests,direction,speed)
456             printf qq{"%4d-%02d-%02d %02d:%02d:%02d",%d,%.2f,%d,%.1s,%.2f,%.2f,%.2f%s},
457             $d[5] + 1900, ++$d[4], @d[3,2,1,0],
458             $host->{id}, $host->{ping},
459 0 0         $n, $dir, $sp, $slow, $fast, $opt_CNL ? "\n" : "\r\n";
460             }
461             elsif ($opt_P) {
462 0           printf do { join "\n", map { " $_" }
  0            
  0            
463             "<result>",
464             " <channel>%s</channel>",
465             " <customUnit>M%s/s</customUnit>",
466             " <float>1</float>",
467             " <value>%0.2f</value>",
468             " </result>\n",
469             }, $dir, $unit->[1], $sp;
470             }
471             else {
472 0 0 0       $opt_q && $opt_v and print $opt_v > 2 ? "\n " : " " x (40 - $opt_q);
    0          
473 0 0 0       $opt_v || !$opt_1 and printf "%-10s %8s M%s/s\n", $dir, $sp, $unit->[1];
474 0 0         $opt_v > 1 and printf " Transfer %10.3f kb in %9.3f s. [%8.3f - %8.3f]\n",
475             $size / 1024, $time, $slow, $fast;
476             }
477 0           return $sp;
478             } # result
479              
480             ### ############################################################################
481              
482             sub get_config {
483 0     0     my $url = "http://www.speedtest.net/speedtest-config.php";
484 0           my $rsp = $ua->request (HTTP::Request->new (GET => $url));
485 0 0         $rsp->is_success or die "Cannot get config: ", $rsp->status_line, "\n";
486 0           my $xml = XMLin ( $rsp->content,
487             keeproot => 1,
488             noattr => 0,
489             keyattr => [ ],
490             suppressempty => "",
491             );
492 0 0         $opt_v > 5 and ddumper $xml->{settings};
493 0           return $xml->{settings};
494             } # get_config
495              
496             sub get_servers {
497 0     0     my $servlist;
498 0           foreach my $url (qw(
499             http://www.speedtest.net/speedtest-servers-static.php
500             http://www.speedtest.net/speedtest-servers.php
501             http://c.speedtest.net/speedtest-servers.php
502             )) {
503 0 0         $opt_v > 2 and warn "Fetching $url\n";
504 0           my $rsp = $ua->request (HTTP::Request->new (GET => $url));
505 0 0         $opt_v > 2 and warn $rsp->status_line, "\n";
506 0 0 0       $rsp->is_success and $servlist = $rsp->content and last;
507             }
508 0 0         $servlist or die "Cannot get any config\n";
509 0           my $xml = XMLin ($servlist,
510             keeproot => 1,
511             noattr => 0,
512             keyattr => [ ],
513             suppressempty => "",
514             );
515             # 4601 => {
516             # cc => 'NL',
517             # country => 'Netherlands',
518             # dist => '38.5028663935342602', # added later
519             # id => 4601,
520             # lat => '52.2167',
521             # lon => '5.9667',
522             # name => 'Apeldoorn',
523             # sponsor => 'Solcon Internetdiensten N.V.',
524             # url => 'http://speedtest.solcon.net/speedtest/upload.php',
525             # url2 => 'http://ooklaspeedtest.solcon.net/speedtest/upload.php'
526             # },
527              
528 0           return map { $_->{id} => $_ } @{$xml->{settings}{servers}{server}};
  0            
  0            
529             } # get_servers
530              
531             sub distance {
532 0     0     my ($lat_c, $lon_c, $lat_s, $lon_s) = @_;
533 0           my $rad = 6371; # km
534              
535             # Convert angles from degrees to radians
536 0           my $dlat = deg2rad ($lat_s - $lat_c);
537 0           my $dlon = deg2rad ($lon_s - $lon_c);
538              
539 0           my $x = sin ($dlat / 2) * sin ($dlat / 2) +
540             cos (deg2rad ($lat_c)) * cos (deg2rad ($lat_s)) *
541             sin ($dlon / 2) * sin ($dlon / 2);
542              
543 0           return $rad * 2 * atan2 (sqrt ($x), sqrt (1 - $x)); # km
544             } # distance
545              
546             sub servers {
547 0     0     my %list = get_servers ();
548 0 0         if (my $iid = $config->{"server-config"}{ignoreids}) {
549 0 0         $opt_v > 3 and warn "Removing servers $iid from server list\n";
550 0           delete @list{split m/\s*,\s*/ => $iid};
551             }
552 0 0         $opt_a or delete @list{grep { $list{$_}{cc} ne $opt_c } keys %list};
  0            
553 0 0         %list or die "No servers in $opt_c found\n";
554 0           for (values %list) {
555             $_->{dist} = distance ($client->{lat}, $client->{lon},
556 0           $_->{lat}, $_->{lon});
557 0           ($_->{url0} = $_->{url}) =~ s{/speedtest/upload.*}{};
558 0 0         $opt_v > 7 and ddumper $_;
559             }
560 0           return %list;
561             } # servers
562              
563             sub servers_by_ping {
564 0     0     my %list = servers;
565 0           my @list = values %list;
566 0 0         $opt_v > 1 and say STDERR "Finding fastest host out of @{[scalar @list]} hosts for $opt_c ...";
  0            
567 0           my $pa = LWP::UserAgent->new (
568             max_redirect => 2,
569             agent => "Opera/25.00 opera 25",
570             parse_head => 0,
571             cookie_jar => {},
572             timeout => $timeout,
573             );
574 0           $pa->env_proxy;
575 0   0       $opt_ping ||= 40;
576 0 0         if (@list > $opt_ping) {
577 0           @list = sort { $a->{dist} <=> $b->{dist} } @list;
  0            
578 0 0         @server or splice @list, $opt_ping;
579             }
580 0           foreach my $h (@list) {
581 0           my $t = 0;
582 0 0 0 0     if (@server and not first { $h->{id} == $_ } @server) {
  0            
583 0           $h->{ping} = 999999;
584 0           next;
585             }
586             $opt_v > 5 and printf STDERR "? %4d %-20.20s %s\n",
587 0 0         $h->{id}, $h->{sponsor}, $h->{url};
588 0           my $req = HTTP::Request->new (GET => "$h->{url}/latency.txt");
589 0           for (0 .. 3) {
590 0           my $t0 = [ gettimeofday ];
591 0           my $rsp = $pa->request ($req);
592 0           my $elapsed = tv_interval ($t0);
593 0 0         $opt_v > 8 and printf STDERR "%4d %9.2f\n", $_, $elapsed;
594 0 0         if ($elapsed >= 15) {
595 0           $t = 40;
596 0           last;
597             }
598 0 0         $t += ($rsp->is_success ? $elapsed : 1000);
599             }
600 0           $h->{ping} = $t * 1000; # report in ms
601             }
602 0           sort { $a->{ping} <=> $b->{ping}
603 0 0         || $a->{dist} <=> $b->{dist} } @list;
604             } # servers_by_ping
605              
606             __END__
607              
608             =encoding UTF-8
609              
610             =head1 NAME
611              
612             App::SpeedTest - Command-line interface to speedtest.net
613              
614             =head1 SYNOPSIS
615              
616             $ speedtest [ --no-geo | --country=NL ] [ --list | --ping ] [ options ]
617              
618             $ speedtest --list
619             $ speedtest --ping --country=BE
620             $ speedtest
621             $ speedtest -s 4358
622             $ speedtest --url=http://ookla.extraip.net
623             $ speedtest -q --no-download
624             $ speedtest -Q --no-upload
625              
626             =head1 DESCRIPTION
627              
628             The provided perl script is a command-line interface to the
629             L<speedtest.net|http://www.speedtest.net/> infrastructure so that
630             flash is not required
631              
632             It was written to feature the functionality that speedtest.net offers
633             without the overhead of flash or java and the need of a browser.
634              
635             =head1 Raison-d'être
636              
637             The tool is there to give you a quick indication of the achievable
638             throughput of your current network. That can drop dramatically if
639             you are behind (several) firewalls or badly configured networks (or
640             network parts like switches, hubs and routers).
641              
642             It was inspired by L<speedtest-cli|https://github.com/sivel/speedtest-cli>,
643             a project written in python. But I neither like python, nor did I like the
644             default behavior of that script. I also think it does not take the right
645             decisions in choosing the server based on distance instead of speed. That
646             B<does> matter if one has fiber lines. I prefer speed over distance.
647              
648             =head1 Command-line Arguments
649             X<CLIA>
650              
651             =over 2
652              
653             =item -? | --help
654             X<-?>
655             X<--help>
656              
657             Show all available options and then exit.
658              
659             =item -V | --version
660             X<-V>
661             X<--version>
662              
663             Show program version and exit.
664              
665             =item --man
666             X<--man>
667              
668             Show the builtin manual using C<pod2man> and C<nroff>.
669              
670             =item --info
671             X<--info>
672              
673             Show the builtin manual using C<pod2text>.
674              
675             =item -v[#] | --verbose[=#]
676             X<-v>
677             X<--version>
678              
679             Set verbose level. Default value is 1. A plain -v without value will set
680             the level to 2.
681              
682             =item --simple
683             X<--simple>
684              
685             An alias for C<-v0>
686              
687             =item --all
688             X<--all>
689              
690             No (default) filtering on available servers. Useful when finding servers
691             outside of the country of your own location.
692              
693             =item -g | --geo
694             X<-g>
695             X<--geo>
696              
697             Use GEO-IP service to find the country your ISP is located. The default
698             is true. If disable (C<--no-geo>), the server to use will be based on
699             distance instead of on latency.
700              
701             =item -cXX | --cc=XX | --country=XX
702             X<-c>
703             X<--cc>
704             X<--country>
705              
706             Pass the ISO country code to select the servers
707              
708             $ speedtest -c NL ...
709             $ speedtest --cc=B ...
710             $ speedtest --country=D ...
711              
712             =item --list-cc
713             X<--list-cc>
714              
715             Fetch the server list and then show the list of countries the servers are
716             located with their country code and server count
717              
718             $ speedtest --list-cc
719             AD Andorra 1
720             AE United Arab Emirates 4
721             :
722             ZW Zimbabwe 6
723              
724             You can then use that code to list the servers in the chosen country, as
725             described below.
726              
727             =item -l | --list
728             X<-l>
729             X<--list>
730              
731             This option will show all servers in the selection with the distance in
732             kilometers to the server.
733              
734             $ speedtest --list --country=IS
735             1: 10661 - Tengir hf Akureyri 1980.02 km
736             2: 21605 - Premis ehf Reykjavík 2039.16 km
737             3: 3684 - Nova Reykjavik 2039.16 km
738             4: 6471 - Gagnaveita Reykjavikur Reykjavik 2039.16 km
739             5: 10650 - Nova VIP Reykjavik 2039.16 km
740             6: 16148 - Hringidan Reykjavik 2039.16 km
741             7: 4818 - Siminn Reykjavik 2039.16 km
742             8: 17455 - Hringdu Reykjavík 2039.16 km
743             9: 4141 - Vodafone Reykjavík 2039.16 km
744             10: 3644 - Snerpa Isafjordur 2192.27 km
745              
746             =item -p | --ping | --ping=40
747             X<-p>
748             X<--ping>
749              
750             Show a list of servers in the selection with their latency in ms.
751             Be very patient if running this with L</--all>.
752              
753             $ speedtest --ping --cc=BE
754             1: 4320 - EDPnet Sint-Niklaas 148.06 km 52 ms
755             2: 12627 - Proximus Brussels 173.04 km 55 ms
756             3: 10986 - Proximus Schaarbeek 170.54 km 55 ms
757             4: 15212 - Telenet BVBA/SPRL Mechelen 133.89 km 57 ms
758             5: 29238 - Arcadiz DIEGEM 166.33 km 58 ms
759             6: 5151 - Combell Brussels 173.04 km 59 ms
760             7: 26887 - Arxus NV Brussels 173.04 km 64 ms
761             8: 4812 - Universite Catholiq… Louvain-La-Neuv 186.87 km 70 ms
762             9: 2848 - Cu.be Solutions Diegem 166.33 km 75 ms
763             10: 12306 - VOO Liège 186.26 km 80 ms
764             11: 24261 - Une Nouvelle Ville… Charleroi 217.48 km 147 ms
765             12: 30594 - Orange Belgium Evere 169.29 km 150 ms
766              
767             If a server does not respond, a very high latency is used as default.
768              
769             This option only shows the 40 nearest servers. The number can be changed
770             as optional argument.
771              
772             $ speedtest --cc=BE --ping=4
773             1: 4320 - EDPnet Sint-Niklaas 148.06 km 53 ms
774             2: 29238 - Arcadiz DIEGEM 166.33 km 57 ms
775             3: 15212 - Telenet BVBA/SPRL Mechelen 133.89 km 62 ms
776             4: 2848 - Cu.be Solutions Diegem 166.33 km 76 ms
777              
778             =item -1 | --one-line
779             X<-1>
780             X<--ono-line>
781              
782             Generate a very short report easy to paste in e.g. IRC channels.
783              
784             $ speedtest -1Qv0
785             DL: 40.721 Mbit/s, UL: 30.307 Mbit/s
786              
787             =item -B | --bytes
788             X<-B>
789             X<--bytes>
790              
791             Report throughput in Mbyte/s instead of Mbit/s
792              
793             =item -C | --csv
794             X<-C>
795             X<--csv>
796              
797             Generate the measurements in CSV format. The data can be collected in
798             a file (by a cron job) to be able to follow internet speed over time.
799              
800             The reported fields are
801              
802             - A timestam (the time the tests are finished)
803             - The server ID
804             - The latency in ms
805             - The number of tests executed in this measurement
806             - The direction of the test (D = Down, U = Up)
807             - The measure avarage speed in Mbit/s
808             - The minimum speed measured in one of the test in Mbit/s
809             - The maximum speed measured in one of the test in Mbit/s
810              
811             $ speedtest -Cs4358
812             "2015-01-24 17:15:09",4358,63.97,40,D,93.45,30.39,136.93
813             "2015-01-24 17:15:14",4358,63.97,40,U,92.67,31.10,143.06
814              
815             =item -U | --skip-undef
816             X<-U>
817             X<--skip-undef>
818              
819             Skip reporting measurements that have no speed recordings at all.
820             The default is to report these as C<0.00> .. C<999999999.999>.
821              
822             =item -P | --prtg
823             X<-P>
824             X<--prtg>
825              
826             Generate the measurements in XML suited for PRTG
827              
828             $ speedtest -P
829             <?xml version="1.0" encoding="UTF-8" ?>
830             <prtg>
831             <text>Testing from My ISP (10.20.30.40)</text>
832             <result>
833             <channel>Ping</channel>
834             <customUnit>ms</customUnit>
835             <float>1</float>
836             <value>56.40</value>
837             </result>
838             <result>
839             <channel>Download</channel>
840             <customUnit>Mbit/s</customUnit>
841             <float>1</float>
842             <value>38.34</value>
843             </result>
844             <result>
845             <channel>Upload</channel>
846             <customUnit>Mbit/s</customUnit>
847             <float>1</float>
848             <value>35.89</value>
849             </result>
850             </prtg>
851              
852             =item --url[=XXX]
853             X<--url>
854              
855             With no value, show server url in list
856              
857             With value, use specific server url: do not scan available servers
858              
859             =item --ip
860             X<--ip>
861              
862             Show IP for server
863              
864             =item -T[#] | --try[=#]
865             X<-T>
866             X<--try>
867              
868             Use the top # (based on lowest latency or shortest distance) from the list
869             to do all required tests.
870              
871             $ speedtest -T3 -c NL -Q2
872             Testing for 80.x.y.z : XS4ALL Internet BV (NL)
873              
874             Using 13218: 26.52 km 25 ms XS4ALL Internet BV
875             Test download .. Download 31.807 Mbit/s
876             Test upload .. Upload 86.587 Mbit/s
877              
878             Using 15850: 26.09 km 25 ms QTS Data Centers
879             Test download .. Download 80.763 Mbit/s
880             Test upload .. Upload 77.122 Mbit/s
881              
882             Using 11365: 26.09 km 27 ms Vancis
883             Test download .. Download 106.022 Mbit/s
884             Test upload .. Upload 82.891 Mbit/s
885              
886             Rank 01: Server: 11365 26.09 km 27 ms, DL: 106.022 UL: 82.891
887             Rank 02: Server: 15850 26.09 km 25 ms, DL: 80.763 UL: 77.122
888             Rank 03: Server: 13218 26.52 km 25 ms, DL: 31.807 UL: 86.587
889              
890             $ speedtest -1v0 -T5
891             DL: 200.014 Mbit/s, UL: 159.347 Mbit/s, SRV: 13218
892             DL: 203.599 Mbit/s, UL: 166.247 Mbit/s, SRV: 15850
893             DL: 207.249 Mbit/s, UL: 134.957 Mbit/s, SRV: 11365
894             DL: 195.490 Mbit/s, UL: 172.109 Mbit/s, SRV: 5972
895             DL: 179.413 Mbit/s, UL: 160.309 Mbit/s, SRV: 2042
896              
897             Rank 01: Server: 15850 26.09 km 30 ms, DL: 203.599 UL: 166.247
898             Rank 02: Server: 5972 26.09 km 32 ms, DL: 195.490 UL: 172.109
899             Rank 03: Server: 13218 26.52 km 23 ms, DL: 200.014 UL: 159.347
900             Rank 04: Server: 11365 26.09 km 31 ms, DL: 207.249 UL: 134.957
901             Rank 05: Server: 2042 51.41 km 33 ms, DL: 179.413 UL: 160.309
902              
903             =item -s# | --server=# | --server=filename
904             X<-s>
905             X<--server>
906              
907             Specify the ID of the server to test against. This ID can be taken from the
908             output of L</--list> or L</--ping>. Using this option prevents fetching the
909             complete server list and calculation of distances. It also enables you to
910             always test against the same server.
911              
912             $ speedtest -1s4358
913             Testing for 80.x.y.z : XS4ALL Internet BV ()
914             Using 4358: 52.33 km 64 ms KPN
915             Test download ........................................Download: 92.633 Mbit/s
916             Test upload ........................................Upload: 92.552 Mbit/s
917             DL: 92.633 Mbit/s, UL: 92.552 Mbit/s
918              
919             This argument may be repeated to test against multile servers, more or less
920             like specifying your own top x (as with C<-T>).
921              
922             $ speedtest -s 22400 -s 1208 -s 13218
923             Testing for 185.x.y.z : Freedom Internet BV ()
924              
925             Using 13218: 80.15 km 32 ms XS4ALL Internet BV
926             Test download ........................................Download 66.833 Mbit/s
927             Test upload ........................................Upload 173.317 Mbit/s
928              
929             Using 1208: 51.19 km 37 ms Qweb | Full-Service Hosting
930             Test download ........................................Download 52.077 Mbit/s
931             Test upload ........................................Upload 195.833 Mbit/s
932              
933             Using 22400: 80.15 km 46 ms Usenet.Farm
934             Test download ........................................Download 96.341 Mbit/s
935             Test upload ........................................Upload 203.306 Mbit/s
936              
937             Rank 01: Server: 22400 80.15 km 46 ms, DL: 96.341 UL: 203.306
938             Rank 02: Server: 1208 51.19 km 37 ms, DL: 52.077 UL: 195.833
939             Rank 03: Server: 13218 80.15 km 32 ms, DL: 66.833 UL: 173.317
940              
941             If you pass a filename, it is expected to reflect a server-like structure as
942             received from the speedtest server-list, possibly completed with upload- and
943             download URL's. You can only pass one filename not consisting of all digits.
944             If you do, all remaining C<-s> arguments are ignored.
945              
946             { cc => "NL",
947             country => "Netherlands",
948             host => "unlisted.host.amsterdam:8080",
949             id => 9999,
950             lat => "52.37316",
951             lon => "4.89122",
952             name => "Amsterdam",
953             ping => 20.0,
954             sponsor => "Dam tot Damloop",
955             url => "http://unlisted.host.amsterdam/speedtest/speedtest/upload.php",
956             url2 => "http://unlisted.host.amsterdam/speedtest/speedtest/upload.php",
957              
958             dl_list => [
959             "http://unlisted.host.amsterdam/files/128.bin",
960             "http://unlisted.host.amsterdam/files/256.bin",
961             # 40 URL's pointing to files in increasing size
962             "http://unlisted.host.amsterdam/files/2G.bin",
963             ],
964             ul_list => [
965             # 40 URL's
966             ],
967             }
968              
969             =item -t# | --timeout=#
970             X<-t>
971             X<--timeout>
972              
973             Specify the maximum timeout in seconds.
974              
975             =item -d | --download
976             X<-d>
977             X<--download>
978              
979             Run the download tests. This is default unless L</--upload> is passed.
980              
981             =item -u | --upload
982             X<-u>
983             X<--upload>
984              
985             Run the upload tests. This is default unless L</--download> is passed.
986              
987             =item -q[#] | --quick[=#] | --fast[=#]
988             X<-q>
989             X<--quick>
990             X<--fast>
991              
992             Don't run the full test. The default test runs 40 tests, sorting on
993             increasing test size (and thus test duration). Long(er) tests may take
994             too long on slow connections without adding value. The default value
995             for C<-q> is 20 but any value between 1 and 40 is allowed.
996              
997             =item -Q[#] | --realquick[=#]
998             X<-Q>
999             X<--realquick>
1000              
1001             Don't run the full test. The default test runs 40 tests, sorting on
1002             increasing test size (and thus test duration). Long(er) tests may take
1003             too long on slow connections without adding value. The default value
1004             for C<-Q> is 10 but any value between 1 and 40 is allowed.
1005              
1006             =item -mXX | --mini=XX
1007             X<-m>
1008             X<--mini>
1009              
1010             Run the speedtest on a speedtest mini server.
1011              
1012             =item --source=XX
1013              
1014             NYI - mentioned for speedtest-cli compatibility
1015              
1016             =back
1017              
1018             =head1 EXAMPLES
1019              
1020             See L</SYNOPSIS> and L<Command-line arguments|/CLIA>
1021              
1022             =head1 DIAGNOSTICS
1023              
1024             ...
1025              
1026             =head1 BUGS and CAVEATS
1027              
1028             Due to language implementation, it may report speeds that are not
1029             consistent with the speeds reported by the web interface or other
1030             speed-test tools. Likewise for reported latencies, which are not
1031             to be compared to those reported by tools like ping.
1032              
1033             =head1 TODO
1034              
1035             =over 2
1036              
1037             =item Improve documentation
1038              
1039             What did I miss?
1040              
1041             =item Enable alternative XML parsers
1042              
1043             XML::Simple is not the recommended XML parser, but it sufficed on
1044             startup. All other API's are more complex.
1045              
1046             =back
1047              
1048             =head1 PORTABILITY
1049              
1050             As Perl has been ported to a plethora of operating systems, this CLI
1051             will work fine on all systems that fulfill the requirement as listed
1052             in Makefile.PL (or the various META files).
1053              
1054             The script has been tested on Linux, HP-UX, AIX, and Windows 7.
1055              
1056             Debian wheezy will run with just two additional packages:
1057              
1058             # apt-get install libxml-simple-perl libdata-peek-perl
1059              
1060             =head1 SEE ALSO
1061              
1062             As an alternative to L<speedtest.net|http://www.speedtest.net/>, you
1063             could consider L<http://compari.tech/speed|http://compari.tech/speed>.
1064              
1065             The L<speedtest-cli|https://github.com/sivel/speedtest-cli> project
1066             that inspired me to improve a broken CLI written in python into our
1067             beloved language Perl.
1068              
1069             =head1 CONTRIBUTING
1070              
1071             =head2 General
1072              
1073             I am always open to improvements and suggestions. Use issues at
1074             L<github issues|https://github.com/Tux/speedtest/issues>.
1075              
1076             =head2 Style
1077              
1078             I will never accept pull request that do not strictly conform to my
1079             style, however you might hate it. You can read the reasoning behind
1080             my preferences L<here|https://tux.nl/style.html>.
1081              
1082             I really don't care about mixed spaces and tabs in (leading) whitespace
1083              
1084             =head1 WARRANTY
1085              
1086             This tool is by no means a guarantee to show the correct speeds. It
1087             is only to be used as an indication of the throughput of your internet
1088             connection. The values shown cannot be used in a legal debate.
1089              
1090             =head1 AUTHOR
1091              
1092             H.Merijn Brand F<E<lt>linux@tux.freedom.nlE<gt>> wrote this for his own
1093             personal use, but was asked to make it publicly available as application.
1094              
1095             =head1 COPYRIGHT AND LICENSE
1096              
1097             Copyright (C) 2014-2026 H.Merijn Brand
1098              
1099             This software is free; you can redistribute it and/or modify
1100             it under the same terms as Perl itself.
1101              
1102             =cut