Public/Network/Send-WolProxyRequest.ps1

#Requires -Version 3
function Send-WolProxyRequest {

  <#
.NOTES
    Created on: 5/21/2014 1:33 PM
    Created by: Adam Bertram
    Filename: Send-WolProxyRequest.ps1
    General Requirements: Read access to a System Center Configuration Manager database
    Requirements:
        Wake On LAN Command Line Utility (http://www.depicus.com/wake-on-lan/wake-on-lan-cmd.aspx)
    Todos:
        Remove the dependency on wolcmd.exe. Use the Net.Sockets.UdpClient object instead.
        Speed this up by using jobs
.DESCRIPTION
    This script is designed to send a WOL magic packet to a specified computer. If the specified computer is not
    on the same network as the originatnating computer, it will attempt to find a "proxy" Windows computer to
    initiate the WOL send. This gets around traditional network multicasting requirements.

    This script currently requires access to a System Center Configuration Manager database. This
    script uses it to find the various network information about the specified computer to wake.

    This script uses a text file to store known good candidates to be used as WOL proxies. It can either
    be prepopulated with Windows PCs or as you use this script more the script will populate it with
    all of the WOL proxies it picks as to speed up the selection process.
.EXAMPLE
    .\Send-WolProxyRequest.ps1 -Computername COMPUTERNAME
.EXAMPLE
    .\Send-WolProxyRequest.ps1 -Computername COMPUTERNAME -UsePsRemoting
.PARAMETER Computername
     This computer name that you'd like to attempt to wake up.
.PARAMETER ConfigMgrSite
    The site code of your ConfigMgr site
.PARAMETER ConfigMgrSiteServer
    The computer name of your ConfigMgr site server hosting your database
.PARAMETER WolCmdFilePath
    The file path where the wolcmd.exe utility is located
.PARAMETER UsePsRemoting
    Use this switch if you'd like to use Powershell remoting to kick off the WOL attempt on the WOL proxy
    rather than using WMI to initiate the remote process
.PARAMETER KnownGoodWolProxyHostsFilePath
    The file path to the text file that contains any Windows computers that were previously
    found to be suitable WOL proxies.
#>

  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
    [string]$Computername,

    [Parameter(Mandatory = $False, ValueFromPipeline = $False, ValueFromPipelineByPropertyName = $False)]
    [string]$ConfigMgrSite = 'UHP',

    [Parameter(Mandatory = $False, ValueFromPipeline = $False, ValueFromPipelineByPropertyName = $False)]
    [ValidateScript({ Test-Connection $_ -Quiet -Count 1 })]
    [string]$ConfigMgrSiteServer = 'CONFIGMANAGER',

    [Parameter(Mandatory = $False, ValueFromPipeline = $False, ValueFromPipelineByPropertyName = $False)]
    [ValidateScript({ Test-Path $_ })]
    [string]$WolCmdFilePath = 'wolcmd.exe',

    [Parameter(Mandatory = $False, ValueFromPipeline = $False, ValueFromPipelineByPropertyName = $False)]
    [switch]$UsePsRemoting = $false,

    [Parameter(Mandatory = $False, ValueFromPipeline = $False, ValueFromPipelineByPropertyName = $False)]
    [string]$KnownGoodWolProxyHostsFilePath = "$($env:USERPROFILE)\desktop\KnownGoodWolProxies.txt"
  )

  begin {

    function ConvertTo-DecimalIP {
      <#
        .Synopsis
          Converts a Decimal IP address into a 32-bit unsigned integer.
        .Description
          ConvertTo-DecimalIP takes a decimal IP, uses a shift-like operation on each octet and returns a single UInt32 value.
        http://www.indented.co.uk/2010/01/23/powershell-subnet-math/
        .Parameter IPAddress
          An IP Address to convert.
      #>


      [CmdLetBinding()]
      param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [Net.IPAddress]$IPAddress
      )

      process {
        $i = 3; $DecimalIP = 0;
        $IPAddress.GetAddressBytes() | ForEach-Object { $DecimalIP += $_ * [Math]::Pow(256, $i); $i-- }

        return [UInt32]$DecimalIP
      }
    }

    function ConvertTo-DottedDecimalIP {
      <#
        .Synopsis
          Returns a dotted decimal IP address from either an unsigned 32-bit integer or a dotted binary string.
        .Description
          ConvertTo-DottedDecimalIP uses a regular expression match on the input string to convert to an IP address.
        http://www.indented.co.uk/2010/01/23/powershell-subnet-math/
        .Parameter IPAddress
          A string representation of an IP address from either UInt32 or dotted binary.
      #>


      [CmdLetBinding()]
      param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [String]$IPAddress
      )

      process {
        Switch -RegEx ($IPAddress) {
          "([01]{8}.){3}[01]{8}" {
            return [String]::Join('.', $($IPAddress.Split('.') | ForEach-Object { [Convert]::ToUInt32($_, 2) }))
          }
          "\d" {
            $IPAddress = [UInt32]$IPAddress
            $DottedIP = $(For ($i = 3; $i -gt -1; $i--) {
                $Remainder = $IPAddress % [Math]::Pow(256, $i)
                        ($IPAddress - $Remainder) / [Math]::Pow(256, $i)
                $IPAddress = $Remainder
              })

            return [String]::Join('.', $DottedIP)
          }
          default {
            Write-Error "Cannot convert this format"
          }
        }
      }
    }

    function Get-NetworkAddress {
      <#
        .Synopsis
          Takes an IP address and subnet mask then calculates the network address for the range.
        .Description
          Get-NetworkAddress returns the network address for a subnet by performing a bitwise AND
          operation against the decimal forms of the IP address and subnet mask. Get-NetworkAddress
          expects both the IP address and subnet mask in dotted decimal format.
        http://www.indented.co.uk/2010/01/23/powershell-subnet-math/
        .Parameter IPAddress
          Any IP address within the network range.
        .Parameter SubnetMask
          The subnet mask for the network.
          #>


      [CmdLetBinding()]
      param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [Net.IPAddress]$IPAddress,

        [Parameter(Mandatory = $true, Position = 1)]
        [Alias("Mask")]
        [Net.IPAddress]$SubnetMask
      )

      process {
        [pscustomobject]@{ 'NetworkAddress' = (ConvertTo-DottedDecimalIP ((ConvertTo-DecimalIP $IPAddress) -band (ConvertTo-DecimalIP $SubnetMask))) }
      }
    }

    function ConvertTo-Mask {
      <#
        .Synopsis
          Returns a dotted decimal subnet mask from a mask length.
        .Description
          ConvertTo-Mask returns a subnet mask in dotted decimal format from an integer value ranging
          between 0 and 32. ConvertTo-Mask first creates a binary string from the length, converts
          that to an unsigned 32-bit integer then calls ConvertTo-DottedDecimalIP to complete the operation.
        .Parameter MaskLength
          The number of bits which must be masked.
      #>


      [CmdLetBinding()]
      param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [Alias("Length")]
        [ValidateRange(0, 32)]
        $MaskLength
      )

      Process {
        return ConvertTo-DottedDecimalIP ([Convert]::ToUInt32($(("1" * $MaskLength).PadRight(32, "0")), 2))
      }
    }

    function Get-NetworkRange([String]$IP, [String]$Mask) {
      if ($IP.Contains("/")) {
        $Temp = $IP.Split("/")
        $IP = $Temp[0]
        $Mask = $Temp[1]
      }

      if (!$Mask.Contains(".")) {
        $Mask = ConvertTo-Mask $Mask
      }

      $DecimalIP = ConvertTo-DecimalIP $IP
      $DecimalMask = ConvertTo-DecimalIP $Mask

      $Network = $DecimalIP -band $DecimalMask
      $Broadcast = $DecimalIP -bor ((-bnot $DecimalMask) -band [UInt32]::MaxValue)

      for ($i = $($Network + 1); $i -lt $Broadcast; $i++) {
        ConvertTo-DottedDecimalIP $i
      }
    }

    function Get-OfflineComputerNetworkInformation ($Computername) {
      $WmiQuery = "SELECT DISTINCT *
        FROM SMS_R_System AS sys
        JOIN SMS_G_System_NETWORK_ADAPTER_CONFIGURATION AS net ON net.ResourceID = sys.ResourceID
        WHERE sys.Name = '$ComputerName' AND
        net.IPAddress IS NOT NULL"


      $WmiParams = @{
        'ComputerName' = $ConfigMgrSiteServer
        'Namespace'    = "root\sms\site_$ConfigMgrSite"
        'Query'        = $WmiQuery
      }

      ## Query all network interfaces on the local machine and parse out IP address, subnet mask and the MAC
      Write-Verbose "Querying site server '$ConfigMgrSiteServer' with query '$WmiQuery'"
      try {
        $Output = @{ }
        $NetworkInfo = Get-WmiObject @WmiParams
        if (!$NetworkInfo) {
          throw "Computer '$Computername' could not be found in the SCCM database"
        } else {
          $NetworkInfo | ForEach-Object {
            $Output.IPAddress = [string]([regex]'\b(?:\d{1,3}\.){3}\d{1,3}\b').Matches($_.net.IPAddress)
            $Output.SubnetMask = [string]([regex]'\b(?:\d{1,3}\.){3}\d{1,3}\b').Matches($_.net.IPSubnet)
            $Output.MACAddress = [string](($_.net.MACAddress.replace(":", "")).replace("-", "")).replace(".", "")
          }
        }
        [pscustomobject]$Output
      } catch {
        Write-Error $_.Exception.Message
      }
    }

    function Get-LocalIpNetwork {
      Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter "IPEnabled = 'True'" | Where-Object { $_.IPAddress -and $_.IPSubnet } | ForEach-Object {
        [pscustomobject]@{ 'LocalIPNetwork' = (Get-NetworkAddress -IPAddress $_.IPAddress[0] -SubnetMask $_.IPSubnet[0]) }
      }
    }

    function Test-Ping {
      param ($ComputerName)
      try {
        $oPing = New-Object system.net.networkinformation.ping;
        if (($oPing.Send($ComputerName, 200).Status -eq 'TimedOut')) {
          $false
        } else {
          $true
        }
      } catch [System.Exception] {
        $false
      }##endtry
    }

    function Test-Wmi ($IpAddress) {
      try {
        $Result = ([WMICLASS]"\\$IpAddress\Root\CIMV2:Win32_Process").create("hostname")
        if ($Result.ReturnValue -eq 0) {
          $true
        } else {
          $false
        }
      } catch {
        $false
      }
    }

    function Validate-IsValidHost ($IpAddress) {
      try {
        Write-Verbose "Testing $IpAddress Ping"
        if (Test-Ping -ComputerName $IpAddress) {
          Write-Verbose "Testing $IpAddress Ping - Success"
          ## Assume if the C$ share is available, it's a Windows computer we can
          ## copy the wolcmd utility to and run. This could be better.
          Write-Verbose "Testing $IpAddress SMB share file copy and removal"
          ## Create a temp text file and try to copy it over to test access
          $TestFilePath = "$($env:SystemDrive)\testcopy.txt"
          Add-Content -Path $TestFilePath -Value '' -Force
          Copy-Item -Path $TestFilePath -Destination "\\$IpAddress\c$" -Force
          Remove-Item -Path "\\$IpAddress\c$\testcopy.txt" -Force
          ## If it hasn't thrown an error yet then we've confirmed we can copy and delete a file from it
          Write-Verbose "Testing $IpAddress SMB share file copy and removal - Success"
          Write-Verbose "Testing $IpAddress remote WMI process creation"
          if (Test-Wmi -IpAddress $IpAddress) {
            Write-Verbose "Testing $IpAddress remote WMI process creation - Success"
            Write-Verbose "All tests passed. $IpAddress is a good WOL proxy"
            $true
          } else {
            throw 'Remote process could not be created with WMI'
          }
        } else {
          throw 'Host is offline'
        }
      } catch {
        Write-Warning "Test failed with error '$($_.Exception.Message)' for $IpAddress"
        $false
      }
    }

    function Get-WolProxy ($IpAddress, $SubnetMask) {
      ## Check if any known good WOL proxy exists before scanning all the IPs
      ## in that network
      $IpNetwork = Get-NetworkAddress -IPAddress $IpAddress -SubnetMask $SubnetMask
      $KnownGoodWolProxy = Get-KnownGoodWolProxy -IpNetwork $IpNetwork.NetworkAddress
      if ($KnownGoodWolProxy) {
        return $KnownGoodWolProxy
      } else {
        $HostIps = Get-NetworkRange -IP $IpAddress -Mask $SubnetMask
        foreach ($Ip in $HostIps) {
          Write-Verbose "Checking $Ip if good candidate for WOL proxy..."
          ## Check to see if our WOL proxy PC is online
          if (Validate-IsValidHost -IpAddress $Ip) {
            Write-Verbose "WOL Proxy found: $Ip"

            ## Add this computer to the known good proxy list for later
            New-KnownGoodWolProxy -HostProxy @{ 'IpAddress' = $Ip; 'SubnetMask' = $SubnetMask }

            return $Ip
          } else {
            Write-Verbose "IP address '$Ip' will not work as a WOL proxy"
          }
        }
        $false
      }
    }

    ## WOL proxy Windows hosts that are online and accessible
    function New-KnownGoodWolProxy {
      [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
      [CmdletBinding()]
      param (
        [hashtable]$HostProxy
      )
      if (!(Get-KnownGoodWolProxy $HostProxy.IpAddress)) {
        Write-Verbose "The '$($HostProxy.IpAddress)' host is not yet in the known good WOL proxy host file."
        ## Create IP network from IP address and subnet mask
        $IpNetwork = Get-NetworkAddress -IPAddress $HostProxy.IpAddress -SubnetMask $HostProxy.SubnetMask
        Write-Verbose "Adding IP Address: $($HostProxy.IpAddress) IP Network: $IpNetwork SubnetMask: $($HostProxy.SubnetMask) to known good WOL proxy host file"
        [pscustomobject]@{
          'IpAddress'  = $HostProxy.IpAddress;
          'IpNetwork'  = $IpNetwork.NetworkAddress;
          'SubnetMask' = $HostProxy.SubnetMask
        } | Export-Csv -Path $KnownGoodWolProxyHostsFilePath -Append -NoTypeInformation
      }
    }

    function Remove-KnownGoodWolProxy ($IpAddress) {
      $NewCsvContents = Import-Csv -Path $KnownGoodWolProxyHostsFilePath | Where-Object { $_.IpAddress -ne $IpAddress }
      ##TODO: This removes the row and headers if only 1 row exists
      $NewCsvContents | Export-Csv -Path $KnownGoodWolProxyHostsFilePath -NoTypeInformation
    }

    ## Searches the known good WOL proxy file for an accessible host in the IP network specified
    function Get-KnownGoodWolProxy ([string]$IpNetwork) {
      if (!(Test-Path $KnownGoodWolProxyHostsFilePath)) {
        Write-Verbose "Known good WOL proxy host file at '$KnownGoodWolProxyHostsFilePath' does not exist"
        $false
      } else {
        $Hosts = Import-Csv -Path $KnownGoodWolProxyHostsFilePath | Where-Object { $_.IpNetwork -eq $IpNetwork }
        if (!$Hosts) {
          Write-Verbose "No known good WOL proxy hosts found in IP network '$IpNetwork'"
          $false
        } else {
          $HostsOnNet = $Hosts | Where-Object { $_.IpNetwork -eq $IpNetwork }
          if ($HostsOnNet) {
            Write-Verbose "$(($HostsOnNet | Measure-Object -Sum -ea silentlycontinue).Count) (unknown accessibility) known good WOL proxy hosts found on IP network '$IpNetwork'"
            Write-Verbose 'Checking previously known good WOL proxy hosts if still usable'
            $AccessibleHost = $HostsOnNet | Where-Object { Validate-IsValidHost $_.IpAddress } | Select-Object -First 1 -ExpandProperty IpAddress
            if ($AccessibleHost) {
              Write-Verbose "Found hostname '$AccessibleHost' still to be a good WOL proxy host"
              $AccessibleHost
            } else {
              Remove-KnownGoodWolProxy -IpAddress $_.IpAddress
            }
          } else {
            Write-Verbose "No accessible, known good WOL proxy hosts on the '$IpNetwork' found"
          }
        }
      }
    }

    function Send-WolPacketLocally ($MacAddress, $IpNetwork, $SubnetMask) {
      & $WolCmdFilePath $MacAddress $IPNetwork $SubnetMask $WolUdpPort 2>&1> $null
    }

    function Invoke-WolProxy ($IpAddress, $OfflineMacAddress, $OfflineIpNetwork, $OfflineSubnetMask) {
      ## Remove the dependency on wolcmd.exe. Use the Net.Sockets.UdpClient object instead.
      ## Copy wolcmd to the remote proxy computer
      Write-Verbose "Copying $WolCmdFilePath to \\$IpAddress\c`$..."
      Copy-Item $WolCmdFilePath "\\$IpAddress\c$" -Force

      $WolCmdString = "C:\$($WolCmdFilePath | Split-Path -Leaf) $OfflineMacAddress $OfflineIPNetwork $OfflineSubnetMask $WolUdpPort"
      Write-Verbose "Initiating the string `"$WolCmdString`"..."
      Write-Verbose "Connecting to $IpAddress and attempting WOL proxy function via WMI RPC method..."
      $Result = ([WMICLASS]"\\$IpAddress\Root\CIMV2:Win32_Process").create($WolCmdString)
      if ($Result) {
        Write-Verbose "Waiting for process ID $($Result.ProcessID) on IP $IpAddress..."
        while (Get-Process -Id $Result.ProcessID -ComputerName $IpAddress -ErrorAction 'SilentlyContinue') {
          sleep 1
        }
        Write-Verbose "Process ID $($Result.ProcessID) has exited"
      } else {
        Write-Warning "Failed to initiate WMI process creation on '$IpAddress'. Exit code was '$($NewProcess.ReturnValue)'"
      }
      #}
      ## Cleanup all files copied to the proxy computer
      Write-Verbose 'Cleaning up file remnants on WOL proxy computer...'
      if (Test-Path "\\$IpAddress\c`$\$($WolCmdFilePath | Split-Path -Leaf)") {
        Remove-Item -Path "\\$IpAddress\c`$\$($WolCmdFilePath | Split-Path -Leaf)" -Force
      }
    }

    if (Test-Connection -ComputerName $Computername -Quiet -Count 1) {
      Write-Verbose -Message "The computer $Computername is already online"
      return
    }

    ## Find all of the IP/subnet masks on the local computer
    $LocalIPAddressNetworks = Get-LocalIpNetwork
    Write-Verbose "Found $($LocalIPAddressNetworks.Count) local IP networks"

    ## Common WOL UDP ports are 7 and 9
    $WolUdpPort = 9

    $OfflineComputerNetwork = Get-OfflineComputerNetworkInformation $Computername
  }
  process {
    try {
      ## TODO: Make this run in parallel by jobs of a foreach -parallel workflow loop
      foreach ($Network in $OfflineComputerNetwork) {
        Write-Verbose "Processing IP address $($Network.IPAddress)..."
        Write-Verbose "Checking the remote network to see if it's on any local IP network..."
        $RemoteIpNetwork = Get-NetworkAddress -IPAddress $Network.IpAddress -SubnetMask $Network.SubnetMask
        if ($LocalIPNetworks.LocalIPNetwork -contains $RemoteIpNetwork.NetworkAddress) {
          Write-Verbose 'IP found to be on local subnet. No WOL proxy needed. Sending WOL directly to the intended machine...'
          Send-WolPacketLocally -MacAddress $Network.MacAddress -IpNetwork $RemoteIpNetwork.NetworkAddress -SubnetMask $Network.SubnetMask
        } else {
          Write-Verbose 'IP not found to be on local subnet. Getting WOL proxy computer...'
          $WolProxy = Get-WolProxy -IpAddress $Network.IPAddress -SubnetMask $Network.SubnetMask
          if (!$WolProxy) {
            Write-Warning "Unable to find a WOL proxy for '$Computername'"
          } else {
            Invoke-WolProxy -OfflineIpNetwork $RemoteIpNetwork.NetworkAddress -OfflineMacAddress $Network.MacAddress -OfflineSubnetMask $Network.SubnetMask -IpAddress $WolProxy
          }
        }
      }
    } catch {
      Write-Error $_.Exception.Message
    }
  }
}