Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions PiHoleShell/PiHoleShell.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Export-ModuleMember -Function @(
#DnsControl
'Get-PiHoleDnsBlockingStatus', 'Set-PiHoleDnsBlocking', `
#Config
'Get-PiHoleConfig', `
'Get-PiHoleConfig', 'Set-PiHoleConfig', 'Add-PiHoleConfigArrayItem', 'Remove-PiHoleConfigArrayItem', 'Get-PiHoleConfigProperty', `
#Padd
'Get-PiHolePadd', `
#Metrics
Expand All @@ -40,5 +40,9 @@ Export-ModuleMember -Function @(
#Teleporter
'Get-PiHoleTeleporterDownload', `
#DomainManagement
'Get-PiHoleDomain', 'New-PiHoleDomain', 'Update-PiHoleDomain', 'Remove-PiHoleDomain'
'Get-PiHoleDomain', 'New-PiHoleDomain', 'Update-PiHoleDomain', 'Remove-PiHoleDomain', `
#ClientManagement
'Get-PiHoleClient', 'New-PiHoleClient', 'Update-PiHoleClient', 'Remove-PiHoleClient', 'Get-PiHoleClientSuggestion', `
#NetworkInformation
'Get-PiHoleNetworkGateway', 'Get-PiHoleNetworkRoute', 'Get-PiHoleNetworkInterface', 'Get-PiHoleNetworkDevice', 'Remove-PiHoleNetworkDevice'
)
38 changes: 38 additions & 0 deletions PiHoleShell/Private/Misc.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,44 @@ function ConvertTo-PiHolePascalCaseObject {
}
}

function ConvertTo-PiHoleFriendlyErrorMessage {
#INTERNAL FUNCTION
#
# Pi-hole's own error responses often carry a clearer message/hint than the generic HTTP
# exception text (e.g. "Unable to change configuration (read-only): ...app_sudo is false"
# vs just "403 Forbidden"). This extracts and combines them when present, and adds a
# concrete pointer to fix the most common cause of a blocked config write - the app
# password's app_sudo setting - since Pi-hole's own hint says what's wrong but not how to
# fix it.
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "", Justification = "Falls back to the raw exception message when the response body isn't valid JSON - nothing to report.")]
param (
[Parameter(Mandatory = $true)]
$ErrorRecord
)

$Message = $ErrorRecord.Exception.Message

if ($ErrorRecord.ErrorDetails.Message) {
try {
$ApiError = ($ErrorRecord.ErrorDetails.Message | ConvertFrom-Json).error
if ($ApiError.message) {
$Message = $ApiError.message
if ($ApiError.hint) {
$Message += ": $($ApiError.hint)"
}
if ($ApiError.hint -like '*app_sudo*') {
$Message += " Enable it in your Pi-hole admin UI under Settings > All Settings by searching for 'app_sudo' and setting webserver.api.app_sudo to true."
}
}
}
catch {
# ErrorDetails.Message wasn't valid JSON - fall back to the raw exception message
}
}

return $Message
}

function Remove-PiHoleCurrentAuthSession {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification = "It removes sessions from PiHole only")]
[CmdletBinding()]
Expand Down
97 changes: 97 additions & 0 deletions PiHoleShell/Public/ClientManagement/Get-PiHoleClient.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
function Get-PiHoleClient {
<#
.SYNOPSIS
Get clients

.DESCRIPTION
Request Pi-hole's configured clients (used to apply group-based rules to specific devices).
Omit -Client to get every configured client; specify it to get just that one.

.PARAMETER PiHoleServer
The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100"

.PARAMETER Password
The API Password you generated from your PiHole server

.PARAMETER Client
A specific client to return - an IP address, IP subnet (CIDR), MAC address, hostname, or
interface (prefixed with a colon, e.g. ":eth0"). Omit to return every configured client

.PARAMETER IgnoreSsl
Set to $true to skip SSL certificate validation

.PARAMETER RawOutput
This will dump the response instead of the formatted object

.EXAMPLE
Get-PiHoleClient -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password"

.EXAMPLE
Get-PiHoleClient -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Client "192.168.1.50"
#>
[CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/clients/-client-')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
param (
[Parameter(Mandatory = $true)]
[System.URI]$PiHoleServer,
[Parameter(Mandatory = $true)]
[string]$Password,
[string]$Client,
[bool]$IgnoreSsl = $false,
[bool]$RawOutput = $false
)
try {
$Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl

$Groups = Get-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl

$Uri = "$($PiHoleServer.OriginalString)/api/clients"
if ($Client) {
$Uri += "/$([System.Uri]::EscapeDataString($Client))"
}

$Params = @{
Headers = @{sid = $($Sid) }
Uri = $Uri
Method = "Get"
SkipCertificateCheck = $IgnoreSsl
ContentType = "application/json"
}

$Response = Invoke-RestMethod @Params

if ($RawOutput) {
Write-Output $Response
}

else {
$ObjectFinal = foreach ($Item in $Response.clients) {
$GroupNames = [System.Collections.ArrayList]@()
foreach ($Group in $Item.groups) {
$GroupNames += ($Groups | Where-Object { $_.Id -eq $Group }).Name
}

[PSCustomObject]@{
Client = $Item.client
Name = $Item.name
Comment = $Item.comment
Groups = $GroupNames
Id = $Item.id
DateAdded = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_added).LocalTime
DateModified = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_modified).LocalTime
}
}
Write-Output $ObjectFinal
}
}

catch {
Write-Error -Message $_.Exception.Message
}

finally {
if ($Sid) {
Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
}
}
}
75 changes: 75 additions & 0 deletions PiHoleShell/Public/ClientManagement/Get-PiHoleClientSuggestion.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
function Get-PiHoleClientSuggestion {
<#
.SYNOPSIS
Get client suggestions

.DESCRIPTION
Returns a list of clients Pi-hole has seen making DNS queries but that aren't yet configured as
a client (see New-PiHoleClient to add one).

.PARAMETER PiHoleServer
The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100"

.PARAMETER Password
The API Password you generated from your PiHole server

.PARAMETER IgnoreSsl
Set to $true to skip SSL certificate validation

.PARAMETER RawOutput
This will dump the response instead of the formatted object

.EXAMPLE
Get-PiHoleClientSuggestion -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password"
#>
[CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/clients/_suggestions')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
param (
[Parameter(Mandatory = $true)]
[System.URI]$PiHoleServer,
[Parameter(Mandatory = $true)]
[string]$Password,
[bool]$IgnoreSsl = $false,
[bool]$RawOutput = $false
)
try {
$Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl

$Params = @{
Headers = @{sid = $($Sid) }
Uri = "$($PiHoleServer.OriginalString)/api/clients/_suggestions"
Method = "Get"
SkipCertificateCheck = $IgnoreSsl
ContentType = "application/json"
}

$Response = Invoke-RestMethod @Params

if ($RawOutput) {
Write-Output $Response
}

else {
$ObjectFinal = foreach ($Item in $Response.clients) {
[PSCustomObject]@{
HwAddr = $Item.hwaddr
MacVendor = $Item.macVendor
LastQuery = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.lastQuery).LocalTime
Addresses = $Item.addresses
Names = $Item.names
}
}
Write-Output $ObjectFinal
}
}

catch {
Write-Error -Message $_.Exception.Message
}

finally {
if ($Sid) {
Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
}
}
}
121 changes: 121 additions & 0 deletions PiHoleShell/Public/ClientManagement/New-PiHoleClient.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
function New-PiHoleClient {
<#
.SYNOPSIS
Add a new client

.DESCRIPTION
Adds a client to Pi-hole so group-based rules can be applied to it specifically. A client may
be identified by IP address, IP subnet (CIDR notation), MAC address, hostname, or the interface
it connects through (prefixed with a colon, e.g. ":eth0"). IP-based recognition is preferred -
MAC address, hostname, and interface recognition only work for devices Pi-hole has already seen.

.PARAMETER PiHoleServer
The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100"

.PARAMETER Password
The API Password you generated from your PiHole server

.PARAMETER Client
The client to add - an IP address, IP subnet (CIDR), MAC address, hostname, or interface
(prefixed with a colon, e.g. ":eth0")

.PARAMETER Comment
An optional comment to store alongside the client

.PARAMETER Group
The group(s) this client applies to. Defaults to "Default"

.PARAMETER IgnoreSsl
Set to $true to skip SSL certificate validation

.PARAMETER RawOutput
This will dump the response instead of the formatted object

.EXAMPLE
New-PiHoleClient -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Client "192.168.1.50" -Comment "Kid's tablet"
#>
[CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#post-/clients')]
[Diagnostics.CodeAnalysis.SuppressMessage("PSUseShouldProcessForStateChangingFunctions", "", Justification = "Ignoring for now")]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
param (
[Parameter(Mandatory = $true)]
[System.URI]$PiHoleServer,
[Parameter(Mandatory = $true)]
[string]$Password,
[Parameter(Mandatory = $true)]
[string]$Client,
[string]$Comment = $null,
[string[]]$Group = "Default",
[bool]$IgnoreSsl = $false,
[bool]$RawOutput = $false
)
try {
$FindMatchingClient = Get-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl -Client $Client

if ($FindMatchingClient) {
throw "Client $Client already exists on $PiHoleServer! Please use Update-PiHoleClient to update it"
}

$AllGroups = Get-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl
$AllGroupsNames = @()
$AllGroupsIds = @()
foreach ($GroupItem in $Group) {
$FoundGroup = $AllGroups | Where-Object { $_.Name -eq $GroupItem }
if ($FoundGroup) {
$AllGroupsNames += $FoundGroup.Name
$AllGroupsIds += $FoundGroup.Id
}
else {
throw "Cannot find $GroupItem on $PiHoleServer! Please use Get-PiHoleGroup to list all groups"
}
}

$Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl

$Body = @{
client = $Client
comment = $Comment
groups = [Object[]]($AllGroupsIds)
}

$Params = @{
Headers = @{sid = $($Sid) }
Uri = "$($PiHoleServer.OriginalString)/api/clients"
Method = "Post"
SkipCertificateCheck = $IgnoreSsl
Body = $Body | ConvertTo-Json -Depth 10
ContentType = "application/json"
}

$Response = Invoke-RestMethod @Params

if ($RawOutput) {
Write-Output $Response
}

else {
$ObjectFinal = foreach ($Item in $Response.clients) {
[PSCustomObject]@{
Client = $Item.client
Name = $Item.name
Comment = $Item.comment
Groups = $AllGroupsNames
Id = $Item.id
DateAdded = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_added).LocalTime
DateModified = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_modified).LocalTime
}
}
Write-Output $ObjectFinal
}
}

catch {
Write-Error -Message $_.Exception.Message
}

finally {
if ($Sid) {
Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
}
}
}
Loading
Loading