#!/usr/bin/env perl # # Check an Ookla Speedtest server with a specified URL and/or host is present # on the list of servers. # # Author: Tom Ryder # Copyright: 2018 Tom Ryder # package main; # Force me to write this properly use strict; use warnings; use utf8; # Require at least this Perl version # Should work even on very old Perls use 5.006; # Import required modules use English '-no_match_vars'; use LWP::UserAgent (); use Monitoring::Plugin qw(%ERRORS); use XML::LibXML; # Decree package version our $VERSION = '0.05'; # Add description and license package variables our $DESCRIPTION = <<'EOF'; This plugin retrieves the list of speedtest servers from speedtest.net and checks for the presence of at least one server with the given host and/or URL. EOF our $LICENSE = <<'EOF'; This plugin is distributed under an MIT license. See LICENSE, or visit . Thanks to Inspire Net Ltd for allowing this open-source fork. EOF # Define custom options our @OPTS = ( { spec => 'host|h=s', label => 'HOSTNAME:PORT', help => 'Hostname:port pair to find in list, usually *:8080', }, { spec => 'url|u=s', label => 'URL', help => 'URL to find in list, usually ends in /upload.php', }, ); # URL from which the server list should be retrieved our $SERVERS_LIST_URL = 'https://www.speedtest.net/speedtest-servers.php'; # Build Monitoring::Plugin object my $mp = Monitoring::Plugin->new( usage => 'Usage: %s --host|-H HOSTNAME:PORT --url|-u URL', version => $VERSION, blurb => $DESCRIPTION, license => $LICENSE, ) or die "Failed plugin construct\n"; # Anything that dies in here will raise ->plugin_die() eval { # Define and parse custom options for my $opt (@OPTS) { $mp->add_arg( %{$opt} ); } $mp->getopts(); # At least one of --host and --url must be specified length $mp->opts->host or length $mp->opts->url or die "One or both --host or --url must be specified\n"; # Build a user agent that accepts only XML my $ua = LWP::UserAgent->new(); # Attempt to retrieve the server list my $headers = HTTP::Headers->new( 'Accept' => 'application/xml;text/xml', 'Accept-Encoding' => scalar HTTP::Message::decodable(), ); my $request = HTTP::Request->new( 'GET', $SERVERS_LIST_URL, $headers ); my $response = $ua->request($request); $response->is_success or die "$response->status_line\n"; # Parse the server list as an XML document my $lxml = XML::LibXML->new(); my $doc = $lxml->load_xml( string => $response->decoded_content ) or die "Failed to parse response XML\n"; # Build an XPath query object my $xpc = XML::LibXML::XPathContext->new($doc) or die "Failed to build XPath query object on response XML\n"; # Build a query depending on which options we were provided ## no critic (RequireInterpolationOfMetachars) my $query = '/settings/servers/server'; if ( $mp->opts->url ) { $query .= sprintf q{[@url='%s']}, $mp->opts->url; } if ( $mp->opts->host ) { $query .= sprintf q{[@host='%s']}, $mp->opts->host; } # Check that we have at least one matching server my @servers = $xpc->findnodes($query); my $code = $mp->check_threshold( check => scalar @servers, critical => '1:', ); my $message = sprintf '%u matching servers', scalar @servers; $mp->add_message( $code, $message ); # Add OK-level messages showing the conditions we applied # If we find an actual problem, "OK" will get replaced by ->check_messages() if ( $mp->opts->host ) { $mp->add_message( $ERRORS{OK}, sprintf 'host=%s', $mp->opts->host ); } if ( $mp->opts->url ) { $mp->add_message( $ERRORS{OK}, sprintf 'url=%s', $mp->opts->url ); } # Exit with determined code and messages $mp->plugin_exit( $mp->check_messages( join => q{, }, join_all => q{, }, ), ); } or $mp->plugin_die($EVAL_ERROR);