PSWoL.psm1

#Region '.\Private\Convert-HostnameToIP.ps1' -1

function Convert-HostnameToIP {
    <#
    .SYNOPSIS
    Convert a provided hostname to its resolved IP address
    .DESCRIPTION
    We need an IPv4 address in order to get a MAC for WOL. This function attempts to convert a hostname in to an IP address
    .PARAMETER ComputerName
    A computer name to target for WOL. Will attempt to resolve the name to an IP address that will then be queried in ARP to determine a MAC.
    .EXAMPLE
    Convert-HostnameToIP 'ContosoComp'
    192.168.15.10
    #>

    [CmdletBinding()]
    [OutputType([String])]
    param (
        [String]$ComputerName
    )

    try {
        [System.Net.Dns]::GetHostEntry($ComputerName).AddressList.IPAddressToString | Where-Object {
            $_ -notmatch ':'
        }
    } catch {
        return $null
    }
}
#EndRegion '.\Private\Convert-HostnameToIP.ps1' 27
#Region '.\Private\Convert-MacToByte.ps1' -1

function Convert-MacToByte {
    <#
    .SYNOPSIS
    Convert provided MAC address string to bytes
    .DESCRIPTION
    Accepts MAC addresses in multiple string formats and returns a byte array. This function is necessary to provide compatibility across versions of PowerShell.
    .PARAMETER MacAddress
    Mac address to convert to a byte array. Can be in any MAC format.
    .EXAMPLE
    Convert-MacToByte '6D-84-01-BC-D2-CD'
    #>

    [CmdletBinding()]
    param (
        [String]$MacAddress
    )

    try {
        $MacString = [System.Net.NetworkInformation.PhysicalAddress]::Parse(($MacAddress.ToUpper() -replace '[^0-9A-F]',''))
        $MacString.GetAddressBytes()
    } catch {
        throw "An invalid physical address was specified: $MacAddress"
    }
}
#EndRegion '.\Private\Convert-MacToByte.ps1' 24
#Region '.\Private\Get-SettingsPath.ps1' -1

function Get-SettingsPath {
    <#
    .SYNOPSIS
    Determine the path where saved targets are stored.
    .DESCRIPTION
    Depending on the OS, return an appropriate path relative to the user to store a json file containing saved targets.
    .EXAMPLE
    PS> Get-SettingsPath
    C:\Users\Admin\Appdata\Roaming\PSWol\Settings.json
    #>

    [CmdletBinding()]
    [OutputType([String])]
    param (
        # no params
    )

    if ($IsWindows -or $ENV:OS) {
        $Windows = $true
    } else {
        $Windows = $false
    }
    if ($Windows) {
        $SettingsPath = Join-Path -Path $Env:APPDATA -ChildPath "PSWoL\Settings.json"
    } else {
        $SettingsPath = Join-Path -Path ([Environment]::GetEnvironmentVariable("HOME")) -ChildPath ".local/share/powershell/Modules/PSWoL/Settings.json"
    }
    return $SettingsPath
}
#EndRegion '.\Private\Get-SettingsPath.ps1' 29
#Region '.\Private\Resolve-IPToMac.ps1' -1

function Resolve-IPToMac {
    <#
    .SYNOPSIS
    Resolve an IP address to a MAC address
    .DESCRIPTION
    Attempts to find the corresponding MAC address for a given IP address
    .PARAMETER IPAddress
    The IP address to look up in ARP and find a corresponding MAC address to send a WOL packet to.
    Must be an IPv4 address.
    .EXAMPLE
    Resolve-IPToMac '192.168.15.10'
    6D-84-01-BC-D2-CD
    #>

    [CmdletBinding()]
    [OutputType([System.Net.NetworkInformation.PhysicalAddress])]
    param (
        [ValidateScript({
            if ([IPAddress]::TryParse($_, [ref]0)) {
                return $true
            } else {
                throw "Error: Not a valid IP address."
            }
        })]
        [String]$IPAddress
    )

    # check to see if we're on Windows or not
    if ($IsWindows -or $ENV:OS) {
        $Windows = $true
    } else {
        $Windows = $false
    }

    if ($Windows) {
        $Result = Get-NetNeighbor -IPAddress $IPAddress -AddressFamily IPv4 | Where-Object { $_.LinkLayerAddress -ne '' } | Select-Object -First 1
        [System.Net.NetworkInformation.PhysicalAddress]::Parse(($Result.LinkLayerAddress.ToUpper() -replace '[^0-9A-F]',''))
    } else {
        $Arp = Get-Command -Name "arp" | Select-Object -ExpandProperty Source
        $Regex =  '{0}.+{1}' -f $([Regex]::Escape($IPAddress)), '(?<MAC>([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2}))'
        $ArpResult = & $Arp -a
        foreach ($Line in $ArpResult) {
            if ($Line -match $Regex) {
                [System.Net.NetworkInformation.PhysicalAddress]::Parse(($Matches.MAC.ToUpper() -replace '[^0-9A-F]',''))
                continue
            }
        }
    }
}
#EndRegion '.\Private\Resolve-IPToMac.ps1' 49
#Region '.\Private\Resolve-TargetType.ps1' -1

function Resolve-TargetType {
    <#
    .SYNOPSIS
    Determine if a provided string is an IP address, MAC address, or computername (possibly)
    .DESCRIPTION
    Takes an input string and returns a string value representing whether the input was an IP address, MAC address, or computername
    .PARAMETER Target
    Takes the provided target for a WOL packet and determines if it's a MAC address, IP address or computer name.
    .EXAMPLE
    Resolve-TargetType 192.168.15.10
    IPAddress
    #>

    [CmdletBinding()]
    [OutputType([String])]
    param (
        [String]$Target
    )

    $MacReg = '^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})|([0-9a-fA-F]{4}.[0-9a-fA-F]{4}.[0-9a-fA-F]{4})|([0-9a-fA-F]{12})$'
    if ($Target -match $MacReg) {
        return "MacAddress"
    } elseif ([System.Net.IPAddress]::TryParse($Target,[ref]0)) {
        return "IPAddress"
    } else {
        # possibly a computername but it's impossible to tell
        return "ComputerName"
    }
}
#EndRegion '.\Private\Resolve-TargetType.ps1' 29
#Region '.\Public\Get-WolTarget.ps1' -1

function Get-WolTarget {
    <#
    .SYNOPSIS
    Retrieved a saved target from local file
    .DESCRIPTION
    Can retrieve a saved target's MAC by memorable name or if called with no -Name parameter will return a table of all saved targets.
    .PARAMETER Name
    The name to look up in the saved targets file. Must be an exact match
    .EXAMPLE
    PS> Get-WolTarget
    Name Value
    ---- -----
    gibson2 16:12:EB:E0:32:28
    Gibson1 16:12:EB:E0:32:28
 
    # returns all of the saved targets if present.
    .EXAMPLE
    PS> Get-WolTarget -Name gibson1
    Name MAC
    ---- ---
    gibson1 16:12:EB:E0:32:28
 
    # returns a PSCustomobject with the name and MAC of the saved target
    #>

    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        [String]$Name
    )

    try {
        $SettingsFile = Get-SettingsPath
    } catch {
        throw "Could not retrieve settings file path"
    }

    if (Test-Path $SettingsFile) {
        # load existing saved targets
        $JsonData = Get-Content -Path $SettingsFile | ConvertFrom-Json
        $SavedTargets = @{}
        $JsonData.PSObject.Properties | Foreach-Object {
            $SavedTargets.Add($_.Name, $_.Value)
        }
    } else {
        Write-Warning "No saved targets"
        return $null
    }

    if ($Name) {
        if ($SavedTargets.$Name) {
            [PSCustomObject]@{
                Name = $Name
                MAC = $SavedTargets.$Name
            }
        } else {
            Write-Warning "No saved target found by name: $Name"
        }
    } else {
        $SavedTargets
    }
}
#EndRegion '.\Public\Get-WolTarget.ps1' 62
#Region '.\Public\Remove-WolTarget.ps1' -1

function Remove-WolTarget {
    <#
    .SYNOPSIS
    Remove a saved target from the local settings file
    .DESCRIPTION
    Removes a saved target by name from the saved settings file. Optionally can also delete the entire saved settings file.
    .PARAMETER Name
    The name used by the saved target. Needs to be an exact match. If you're not sure, run Get-WolTarget first to list all currently saved targets.
    .PARAMETER DeleteAll
    Switch parameter to delete the saved targets file entirely
    .EXAMPLE
    PS> Remove-WolTarget -Name "Gibson2"
 
    # will remove the saved entry with the name "Gibson2"
    .EXAMPLE
    PS> Remove-WolTarget -DeleteAll
 
    Confirm
    Are you sure you want to perform this action?
    Performing the operation "Remove-Item" on target "C:\Users\Admin\AppData\Roaming\PSWoL\Settings.json".
    [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): y
 
    # after confirming this will delete the saved settings file stored locally
    #>

    [CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'DeleteAll', Justification = 'Switch parameter where variable is not directly called')]
    param (
        [Parameter(Mandatory,Position=0,ParameterSetName="Name")]
        [String]$Name,
        [Parameter(ParameterSetName="Delete")]
        [Switch]$DeleteAll
    )

    try {
        $SettingsFile = Get-SettingsPath
    } catch {
        throw "Could not retrieve settings file path"
    }

    if (Test-Path $SettingsFile) {
        # load existing saved targets
        $JsonData = Get-Content -Path $SettingsFile | ConvertFrom-Json
        $SavedTargets = @{}
        $JsonData.PSObject.Properties | Foreach-Object {
            $SavedTargets.Add($_.Name, $_.Value)
        }
        switch ($PSCmdlet.ParameterSetName) {
            'Name' {
                try {
                    if ($SavedTargets.$Name) {
                        Write-Verbose "Removing saved target Name: $Name MAC: $($SavedTargets.$Name)"
                        $SavedTargets.Remove($Name)
                        if ($SavedTargets.Count -eq 0) {
                            Write-Warning "Last saved entry removed, deleting saved file"
                            Remove-WolTarget -DeleteAll -Confirm:$false
                        } else {
                            Write-Verbose "Saving changes to $($SettingsFile)"
                            $SavedTargets | ConvertTo-Json | Out-File -FilePath $SettingsFile -Force
                        }
                    } else {
                        Write-Warning "No saved target found by name: $Name"
                    }
                } catch {
                    throw $_
                }
            }
            'Delete' {
                if ($PSCmdlet.ShouldProcess($SettingsFile, 'Remove-Item')) {
                    try {
                        Remove-Item -Path $SettingsFile -Force -ErrorAction Stop
                    } catch {
                        throw $_
                    }
                }
            }
        }

    } else {
        Write-Warning "No saved targets"
    }
}
#EndRegion '.\Public\Remove-WolTarget.ps1' 82
#Region '.\Public\Save-WolTarget.ps1' -1

function Save-WolTarget {
    <#
    .SYNOPSIS
    Save a MAC address along with a name for use with Send-WakeOnLan.
    .DESCRIPTION
    Save a MAC address along with a name for use with Send-WakeOnLan. The name is purely decorative and does not have to be resolvable. It does have to be unique amongst saved names.
    .PARAMETER Name
    A name to refer to the saved target by
    .PARAMETER MacAddress
    MAC address of the saved target. Acceptable formats are:
    00:1a:1e:12:af:38
    00-1a-1e-12-af-38
    001A.1E12.AF38
    001A1E12AF38
    .EXAMPLE
    PS> Save-WolTarget -Name "TheGibson" -MacAddress "16:12:EB:E0:32:28"
 
    # will save the decorative name TheGibson with MAC address 16:12:EB:E0:32:28 to a local settings file
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [String]$Name,
        [Parameter(Mandatory)]
        [ValidateScript({
            if ($_ -match '^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})|([0-9a-fA-F]{4}.[0-9a-fA-F]{4}.[0-9a-fA-F]{4})|([0-9a-fA-F]{12})$') {
                return $true
            } else {
                throw "Provided MAC not in an acceptable format."
            }
        })]
        [String]$MacAddress
    )

    try {
        $SettingsFile = Get-SettingsPath
    } catch {
        throw "Could not retrieve settings file path"
    }

    if (Test-Path $SettingsFile) {
        # load existing saved targets
        $JsonData = Get-Content -Path $SettingsFile | ConvertFrom-Json
        $SavedTargets = @{}
        $JsonData.PSObject.Properties | Foreach-Object {
            $SavedTargets.Add($_.Name, $_.Value)
        }
    } else {
        $SavedTargets = @{}
    }

    try {
        $SavedTargets.Add($Name, $MacAddress)
    } catch [System.Management.Automation.MethodInvocationException] {
        throw "$Name already exists in saved targets. If you want want to use this name, remove the current entry with Remove-WolTarget -Name $Name"
    }

    try {
        if (Test-Path $SettingsFile) {
            Write-Verbose "Saving target to $($SettingsFile)"
            $SavedTargets | ConvertTo-Json | Out-File -FilePath $SettingsFile -Force
            } else {
                Write-Verbose "Saving target to $($SettingsFile)"
                New-Item -Path $SettingsFile -Force | Out-Null
                $SavedTargets | ConvertTo-Json | Out-File -FilePath $SettingsFile
            }
    } catch {
        throw $_
    }

}
#EndRegion '.\Public\Save-WolTarget.ps1' 72
#Region '.\Public\Send-WakeOnLan.ps1' -1

function Send-WakeOnLan {
    <#
    .SYNOPSIS
    Send a Wake-On-LAN packet to a target address.
    .DESCRIPTION
    Sends a Wake-On-LAN packet to a target address. Target address can be a MAC address, IP address or potentially a computer name
    .PARAMETER TargetAddress
    Target address to send Wake-On-LAN packet to. Accepts IP address, MAC address or Computer Name.
    IP must be an IPv4 address.
    MAC addresses can be '1234.abde.4321', '12:34:AB:DE:43:21', '12-34-AB-DE-32-21' or '1234ABDE4321' format.
    Computername needs to be able to resolve to a local computer name. If resolution fails, the packet will not send.
    .PARAMETER SavedTarget
    If there are currently saved target addresses via Save-WolTarget you can specify their name(s) with -SavedTarget to send a WoL packet to them.
    .EXAMPLE
    PS> Send-WakeOnLan -TargetAddress '192.168.15.10'
 
    # this will attempt to do an ARP lookup for that IP address and send a WOL packet to it
    .EXAMPLE
    PS> 'E2-5D-0E-B5-E2-E8', 'DD-20-C9-FF-EC-79', '6D-84-01-BC-D2-CD' | Send-WakeOnLan
 
    # this will send a WOL packet to each of the 3 MAC addresses.
    .EXAMPLE
    PS> Send-WakeOnLan -SavedTarget "TheGibson"
 
    # this will check the saved targets for the name "TheGibson" and send a WoL packet to the MAC address associated with that name.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(ValueFromPipeline,Mandatory,ParameterSetName="TargetAddress")]
        [String[]]$TargetAddress,
        [Parameter(ValueFromPipeline,Mandatory,ParameterSetName="SavedTarget")]
        [String[]]$SavedTarget
    )

    begin {
        $UdpClient = [System.Net.Sockets.UdpClient]::new()
        [Byte[]]$BasePacket = (,0xFF * 6)
        $WolTargets = [System.Collections.ArrayList]::new()
    }

    process {
        switch ($PSCmdlet.ParameterSetName) {
            'Targetaddress' {
                foreach ($Target in $TargetAddress) {
                    switch (Resolve-TargetType -Target $Target) {
                        'IPAddress' {
                            $ResolvedMac = Resolve-IPToMac -IPAddress $Target
                            if ($ResolvedMac) {
                                $MacBytes = Convert-MacToByte -MacAddress $ResolvedMac
                                [Void]$WolTargets.Add(
                                    [PSCustomObject]@{
                                        Identifier = $Target
                                        MacAddress = $([System.BitConverter]::ToString($MacBytes))
                                        Packet = [Byte[]]$($BasePacket; $MacBytes * 16)
                                    }
                                )
                            } else {
                                Write-Warning "Unable to determine MAC for $Target"
                            }
                        }
                        'MacAddress' {
                            $MacBytes = Convert-MacToByte -MacAddress $Target
                            if ($MacBytes) {
                                [Void]$WolTargets.Add(
                                    [PSCustomObject]@{
                                        Identifier = $Target
                                        MacAddress = $([System.BitConverter]::ToString($MacBytes))
                                        Packet = [Byte[]]$($BasePacket; $MacBytes * 16)
                                    }
                                )
                            } else {
                                Write-Warning "Unable to convert MAC to bytes: $Target"
                            }
                        }
                        'ComputerName' {
                            $ResolvedIP = Convert-HostnameToIP -ComputerName $Target
                            if ($ResolvedIP) {
                                $Mac = Resolve-IPToMac -IPAddress $ResolvedIP
                                if ($Mac) {
                                    $MacBytes = Convert-MacToByte -MacAddress $Mac
                                    [Void]$WolTargets.Add(
                                        [PSCustomObject]@{
                                            Identifier = $Target
                                            MacAddress = $([System.BitConverter]::ToString($MacBytes))
                                            Packet = [Byte[]]$($BasePacket; $MacBytes * 16)
                                        }
                                    )
                                } else {
                                    Write-Warning "Unable to determine MAC for $ResolvedIP"
                                }
                            } else {
                                Write-Warning "Unable to resolve IP for $Target"
                            }
                        }
                    }
                }
            }
            'SavedTarget' {
                $SavedTargets = foreach ($Target in $SavedTarget) {
                    Get-WolTarget -Name $Target
                }

                foreach ($TargetObj in $SavedTargets) {
                    $MacBytes = Convert-MacToByte -MacAddress $($TargetObj.MAC)
                    [Void]$WolTargets.Add(
                        [PSCustomObject]@{
                            Identifier = $TargetObj.Name
                            MacAddress = $([System.BitConverter]::ToString($MacBytes))
                            Packet = [Byte[]]$($BasePacket; $MacBytes * 16)
                        }
                    )
                }
            }
        }
    }

    end {
        # sent packet to targets
        foreach ($WolTarget in $WolTargets) {
            if ($PSCmdlet.ShouldProcess($($WolTarget.Identifier),"Send WOL Packet")) {
                try {
                    $UdpClient.Connect(([System.Net.IPAddress]::Broadcast),9)
                    [Void]$UdpClient.Send($($WolTarget.Packet), $($WolTarget.Packet.Length))
                    Write-Verbose "Wake-on-LAN packet sent to target:$($WolTarget.Identifier) MAC: $($WolTarget.MacAddress)"
                } catch {
                    Write-Warning "Error sending WOL packet"
                }
            }
        }
        $UdpClient.Close()
        $UdpClient.Dispose()
    }
}
#EndRegion '.\Public\Send-WakeOnLan.ps1' 134