SpeedTester.psm1

#Requires -Version 3
function Start-SpeedTest
{
    <#
        .SYNOPSIS
        Report your internet Download and Upload speed.
  
        .DESCRIPTION
        SpeedTester leverages speedtest.net hosting servers. SpeedTester will identify the closest hosting servers
        to you and pick the one with the lowest latency to perform a test download and upload. It will then report
        the average Download and Upload in Mbps.
             
        Start-SpeedTest does not accept any parameters at this stage.
 
        .EXAMPLE
        Start-SpeedTest
        Runs SpeedTester with default Download and Upload parameters
 
        .EXAMPLE
        Start-SpeedTest -DownloadMB 10 -Upload 10
        Runs SpeedTester with a download size of 10 MB and an Upload size of 10 MB.
        Valid range is 1 - 30 for both Download and Upload represented in MB only.
             
        .EXAMPLE
        Start-SpeedTest -DownloadMB 20 -Verbose
        Runs SpeedTester with a download size of 20 MB with Verbose logging
             
        .INPUTS
        None
  
        .NOTES
        Author: Mark Ukotic
        Website: http://blog.ukotic.net
        Twitter: @originaluko
        GitHub: https://github.com/originaluko/
 
        .LINK
        https://github.com/originaluko/SpeedTester
 
    #>


    [CmdletBinding()]
    Param(
        [Parameter()]
        [ValidateRange(1,30)]
        [Double]$UploadMB = "7.5",
        [ValidateRange(1,30)]
        [Double]$DownloadMB = "7.5"
    )

    # Server distance helper function
    function Get-ServerInfo {
    
        param(
            [Parameter(Mandatory=$true)]
            $servers 
        )
          
        foreach($server in $servers) 
        { 
            $radius = 6371
            [float]$dlat = ([float]$orilat - [float]$server.lat) * 3.14 / 180
            [float]$dlon = ([float]$orilon - [float]$server.lon) * 3.14 / 180
            [float]$a = [math]::Sin([float]$dlat/2) * [math]::Sin([float]$dlat/2) + [math]::Cos([float]$orilat * 3.14 / 180 ) * [math]::Cos([float]$server.lat * 3.14 / 180 ) * [math]::Sin([float]$dlon/2) * [math]::Sin([float]$dlon/2)
            [float]$c = 2 * [math]::Atan2([math]::Sqrt([float]$a ), [math]::Sqrt(1 - [float]$a))
            [float]$d = [float]$radius * [float]$c

            New-Object PSObject -Property @{
                Distance = $d
                Country = $server.country
                Sponsor = $server.sponsor
                Url = $server.url
                Host = $server.host
            }
        }
    }

    # Avg Ping response helper function
    function Get-AvgPing {  
        param(
            [Parameter(Mandatory=$true)]
            $servers 
        )

        foreach ($server in $servers) { 
     
            try {
                Write-Verbose "Testing ping to $server"
                $test = (Test-Connection -ComputerName $server -Count 4 -ErrorAction Stop | measure-Object -Property ResponseTime -Average).average 
                $response = ($test -as [decimal] ) 
            }   
            catch [System.Net.NetworkInformation.PingException] {
                Write-Warning "$server is offline."
            } 
            catch {
                Write-Warning "Ping test failed for $server"
            }   
            finally {
                New-Object PSObject -Property @{
                    'Destination' = $server
                    'Avg' = $response
                }    
                Write-Verbose "$response ms average ping to $server"
            }
        }
    }
    
    # Download Test helper function
    function Get-DataWCAsync{
        param(
            [Parameter(Mandatory=$true)]
            $Url, 
            [switch]$IncludeStats,
            [ValidateRange(1,30)]
            [Double]$DownloadMB = "7.5"
        )
        $global:download = ($DownloadMB * 1mb)
        $global:wc = New-Object Net.WebClient
        $wc.UseDefaultCredentials = $false
        $wc.Headers.Add("Content-Type","application/x-www-form-urlencoded") 
        $wc.Headers.Add("Accept: text/html, application/xhtml+xml, */*")
        $wc.Headers.Add("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0;Windows NT 5.1; .NET CLR 1.0.3705; .NET CLR 1.1.4322)")
        $wc.Headers.Add("Cache-Control", "no-cache")
        $wc.Headers.Add("Referer", "http://www.speedtest.net")
        $start = Get-Date 
        $null = Register-ObjectEvent -InputObject $wc -EventName DownloadProgressChanged -MessageData @{start=$start;includeStats=$includestats} -SourceIdentifier WebClient.DownloadProgressChanged -Action { 
            filter Get-FileSize {
                "{0:N2} {1}" -f $(
                    if ($_ -lt 1kb) { $_, 'Bytes' }
                    elseif ($_ -lt 1mb) { ($_/1kb), 'KB' }
                    elseif ($_ -lt 1gb) { ($_/1mb), 'MB' }
                    elseif ($_ -lt 1tb) { ($_/1gb), 'GB' }
                    elseif ($_ -lt 1pb) { ($_/1tb), 'TB' }
                    else { ($_/1pb), 'PB' }
                )
            }
            $time = ((Get-Date) - $event.MessageData.start)
            $averagespeed = ($eventargs.BytesReceived * 8 / 1MB) / $time.TotalSeconds
            $elapsed = $Time.ToString('hh\:mm\:ss')
            $remainingseconds = ($eventargs.TotalBytesToReceive - $eventargs.BytesReceived) * 8 / 1MB / $averagespeed
            $receivedsize = $eventargs.BytesReceived | Get-FileSize
            $received = $eventargs.BytesReceived
            $totalSize = $download | Get-FileSize 
            $percent = $eventargs.BytesReceived / $download * 100
            $percent = [Math]::Round($percent)  
            
            If ($received -ge $download) {
                $wc.CancelAsync()
            }
                          
            Write-Progress -Activity (" $url {0:N2} Mbps" -f $averagespeed) -Status ("{0} of {1} ({2}% in {3})" -f $receivedSize,$totalsize,$percent,$elapsed) -SecondsRemaining $remainingseconds -PercentComplete $percent
            if ($percent -eq 100){
                Write-Progress -Activity (" $url {0:N2} Mbps" -f $averagespeed) -Status 'Done' -Completed
                
                if ($event.MessageData.includeStats.IsPresent){
                    $global:down = [Math]::Round($averageSpeed, 2) 
                } 
            }
        }    
        $null = Register-ObjectEvent -InputObject $wc -EventName DownloadDataCompleted -SourceIdentifier WebClient.DownloadDataCompleted -Action { 
            Unregister-Event -SourceIdentifier WebClient.DownloadProgressChanged
            Unregister-Event -SourceIdentifier WebClient.DownloadDataCompleted
        }
    
        try  {  
            Write-Verbose "Performing download from $url"
            $wc.DownloadDataAsync($url)  
        }  
        catch [System.Net.WebException]  {  
            Write-Warning "Download of $url failed"  
        }   
        finally  {    
            while ($wc.IsBusy) {}
            $wc.Dispose() 
        }  
    }
    
    # Upload Test helper function
    function Push-DataWCAsync{
        param(
            [Parameter(Mandatory=$true)]
            $url, 
            $byteArray,
            [switch]$includeStats
        )
        $wc = New-Object Net.WebClient
        $wc.UseDefaultCredentials = $false
        $wc.Headers.Add("Content-Type","application/x-www-form-urlencoded") 
        $wc.Headers.Add("Accept: text/html, application/xhtml+xml, */*")
        $wc.Headers.Add("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0;Windows NT 5.1; .NET CLR 1.0.3705; .NET CLR 1.1.4322)")
        $wc.Headers.Add("Cache-Control", "no-cache")
        $wc.Headers.Add("Referer", "http://www.speedtest.net")
        $start = Get-Date 
        $null = Register-ObjectEvent -InputObject $wc -EventName UploadProgressChanged -MessageData @{start=$start;includeStats=$includeStats} -SourceIdentifier WebClient.UploadProgressChanged -Action { 
            filter Get-FileSize {
                "{0:N2} {1}" -f $(
                    if ($_ -lt 1kb) { $_, 'Bytes' }
                    elseif ($_ -lt 1mb) { ($_/1kb), 'KB' }
                    elseif ($_ -lt 1gb) { ($_/1mb), 'MB' }
                    elseif ($_ -lt 1tb) { ($_/1gb), 'GB' }
                    elseif ($_ -lt 1pb) { ($_/1tb), 'TB' }
                    else { ($_/1pb), 'PB' }
                )
            }
            
            $Time = ((Get-Date) - $event.MessageData.start)
            $averageSpeed = ($eventargs.BytesSent * 8 / 1MB) / $Time.TotalSeconds
            $elapsed = $Time.ToString('hh\:mm\:ss')
            $remainingSeconds = ($eventargs.TotalBytesToSend - $eventargs.BytesSent) * 8 / 1MB / $averageSpeed
            $receivedSize = $eventargs.BytesSent | Get-FileSize
            $totalSize = $eventargs.TotalBytesToSend | Get-FileSize  
            $percent = $eventargs.BytesSent / $eventargs.TotalBytesToSend * 100
            $percent = [Math]::Round($percent)   
                       
            Write-Progress -Activity (" $url {0:N2} Mbps" -f $averageSpeed) -Status ("{0} of {1} ({2}% in {3})" -f $receivedSize,$totalSize,$percent,$elapsed) -SecondsRemaining $remainingSeconds -PercentComplete $percent
            if ($eventargs.ProgressPercentage -eq 100){
                Write-Progress -Activity (" $url {0:N2} Mbps" -f $averageSpeed) -Status 'Done' -Completed
                
                if ($event.MessageData.includeStats.IsPresent){
                    $global:upload = [Math]::Round($averageSpeed, 2) 
                }
            }
        }    
        # $null = Register-ObjectEvent -InputObject $wc -EventName UploadDataCompleted -SourceIdentifier WebClient.UploadDataCompleted -Action {
        # Unregister-Event -SourceIdentifier WebClient.UploadProgressChanged -Force
        # Unregister-Event -SourceIdentifier WebClient.UploadDataCompleted -Force
        # }
        
        try  {  
            Write-Verbose "Performing upload to $url"
            $wc.UploadDataAsync($url,'POST',$byteArray) 
        }  
        catch [System.Net.WebException]  {  
            Write-Warning "Upload of $url failed"  
        }   
        finally  { 
            while ($wc.IsBusy) {}   
            $wc.Dispose()  
            Remove-Job * -Force
        }
    }
    
    Write-Output 'Retrieving configuration...'

    $uri = 'http://beta.speedtest.net/speedtest-config.php'
    try {
        [xml]$config = Invoke-WebRequest -Uri $uri -UseBasicParsing -ErrorAction Stop
    }
    catch {
        Write-Error "Could not download configuration from $uri"
        Break
    }

    $ip = $config.settings.client.ip
    $isp = $config.settings.client.isp
    Write-Output "Testing from $isp ($ip)"

    $orilat = $config.settings.client.lat
    $orilon = $config.settings.client.lon

    Write-Output 'Retrieving server list...'

    $uri = 'http://www.speedtest.net/speedtest-servers.php'
    try {
        [xml]$hosts = Invoke-WebRequest -Uri $uri -UseBasicParsing -ErrorAction Stop
    }
    catch {
        Write-Error "Could not download server list from $uri"
        Break
    }

    Write-Output 'Selecting best server...'

    $servers = $hosts.settings.servers.server
        
    # Sort the distance of each server
    $closestserver = Get-ServerInfo -Servers $servers | Sort-Object -Property distance
    
    $servers = $closestserver[0],$closestserver[1],$closestserver[2]
    $serverurlspilt = ($servers).host -split ':8080'
    $servers = $serverurlspilt[0],$serverurlspilt[2],$serverurlspilt[4] 
    
    # Get avg ping response
    $bestserver = Get-AvgPing -servers $servers

    # Getting lazy and just want lowest latency server asap
    $index = 0
    $minvalue = [decimal]::MaxValue
    $bestserver.avg | ForEach-Object { if ($minvalue -gt $_) {$minvalue = $_; $minindex = $index}; $index++ }
     
    $location = $closestserver[$index].sponsor
    $distance = $closestserver[$index].distance
    Write-Output "Hosted by $location [$distance km] ($minvalue ms)"

    $serverurlspilt = ($closestserver[$index]).url -split 'upload'
    $url = $serverurlspilt[0] + "random4000x4000.jpg"
    
    Write-Output 'Testing download speed...'
    Get-DataWCAsync -Url $url -DownloadMB $downloadmb -IncludeStats 
      
    if ($down -eq $null) {
    Write-Output "Oh no, we couldn't calculate download average"}
    else {
        Write-Output "Download: $down Mbps"
    }
 
    # Creating a random byte array to avoid bad download data messing with the upload
    $bytearray = New-Object Byte[] ($uploadmb * 1mb)
    (New-Object Random).NextBytes($bytearray)
    
    $url = ($closestserver[$index]).url
    Write-Output 'Testing upload speed...'
    Push-DataWCAsync -url $url -byteArray $bytearray -includeStats        

    if ($upload -eq $null) {
    Write-Output "Oh no, we couldn't calculate upload average"}
    else {
        Write-Output "Upload: $upload Mbps"
    }
    Write-Output 'Tests Completed' 
}