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
2 changes: 1 addition & 1 deletion 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 Down
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
101 changes: 101 additions & 0 deletions PiHoleShell/Public/Config/Add-PiHoleConfigArrayItem.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
function Add-PiHoleConfigArrayItem {
<#
.SYNOPSIS
Add config array item

.DESCRIPTION
Adds one item to an array-type Pi-hole configuration setting - for example, an upstream DNS
server (dns/upstreams), a local DNS record (dns/hosts), or a CNAME record (dns/cnameRecords).
Use Set-PiHoleConfig instead for non-array settings.

Requires your app password to have "app_sudo" enabled - Pi-hole blocks config changes from app
passwords by default. 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.

.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 Element
The array-type configuration setting to add to, as a slash-separated path (e.g.
"dns/upstreams", "dns/hosts", or "dns/cnameRecords")

.PARAMETER Value
The item to add. For dns/hosts this is "<ip> <hostname>"; for dns/cnameRecords this is
"<alias>,<target>[,<ttl>]"

.PARAMETER Restart
Whether to restart FTL immediately if this change requires it. Defaults to $true. Set to
$false to defer the restart, e.g. when adding several items independently rather than all at
once - you'll need to restart FTL manually later for the changes to take effect

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

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

.EXAMPLE
Add-PiHoleConfigArrayItem -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Element "dns/cnameRecords" -Value "alias.lan,target.lan"
#>
[CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#put-/config/-element-/-value-')]
[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]$Element,
[Parameter(Mandatory = $true)]
[string]$Value,
[Nullable[bool]]$Restart,
[bool]$IgnoreSsl = $false,
[bool]$RawOutput = $false
)
try {
$Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl

$Uri = "$($PiHoleServer.OriginalString)/api/config/$($Element.Trim('/'))/$([System.Uri]::EscapeDataString($Value))"
if ($PSBoundParameters.ContainsKey('Restart')) {
$Uri += "?restart=$($Restart.ToString().ToLower())"
}

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

$Response = Invoke-RestMethod @Params

if ($RawOutput) {
Write-Output $Response
}
else {
# A successful add returns 201 Created with no body, so there's no response to
# build a rich object from.
$Object = [PSCustomObject]@{
Element = $Element
Value = $Value
Status = "Added"
}
Write-Output $Object
}
}

catch {
Write-Error -Message (ConvertTo-PiHoleFriendlyErrorMessage -ErrorRecord $_)
}

finally {
if ($Sid) {
Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
}
}
}
26 changes: 24 additions & 2 deletions PiHoleShell/Public/Config/Get-PiHoleConfig.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,22 @@ Get current configuration of Pi-hole
Request Pi-hole's full configuration tree (dns, dhcp, ntp, resolver, database, webserver,
files, misc, and debug settings). The formatted output mirrors the API response as nested
objects with PascalCase property names, so the entire configuration is available for
inspection rather than a hand-picked subset.
inspection rather than a hand-picked subset. Pass -Element to request just one subset of
the tree instead of everything.

.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 Element
Only return this part of the configuration tree, as a slash-separated path (e.g.
"dns/upstreams" or "dns/hosts"). Omit to return the entire configuration

.PARAMETER Detailed
Include detailed information about the configuration (e.g. value types and validation info)

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

Expand All @@ -26,6 +34,9 @@ Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-a

.EXAMPLE
(Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password").Dns.Upstreams

.EXAMPLE
Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Element "dns/upstreams"
#>
[CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/config')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
Expand All @@ -34,15 +45,26 @@ Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-a
[System.URI]$PiHoleServer,
[Parameter(Mandatory = $true)]
[string]$Password,
[string]$Element,
[Nullable[bool]]$Detailed,
[bool]$IgnoreSsl = $false,
[bool]$RawOutput = $false
)

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

$Uri = "$($PiHoleServer.OriginalString)/api/config"
if ($Element) {
$Uri += "/$($Element.Trim('/'))"
}
if ($PSBoundParameters.ContainsKey('Detailed')) {
$Uri += "?detailed=$($Detailed.ToString().ToLower())"
}

$Params = @{
Headers = @{sid = $($Sid) }
Uri = "$($PiHoleServer.OriginalString)/api/config"
Uri = $Uri
Method = "Get"
SkipCertificateCheck = $IgnoreSsl
ContentType = "application/json"
Expand Down
73 changes: 73 additions & 0 deletions PiHoleShell/Public/Config/Get-PiHoleConfigProperty.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
function Get-PiHoleConfigProperty {
<#
.SYNOPSIS
Get special properties of your Pi-hole configuration

.DESCRIPTION
Returns the configuration properties that cannot be changed through the API (e.g. because
they're only settable in pihole.toml, or are controlled by an environment variable), along with
why each one is restricted.

.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-PiHoleConfigProperty -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password"
#>
[CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/config/_properties')]
[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/config/_properties"
Method = "Get"
SkipCertificateCheck = $IgnoreSsl
ContentType = "application/json"
}

$Response = Invoke-RestMethod @Params

if ($RawOutput) {
Write-Output $Response
}
else {
$ObjectFinal = foreach ($Item in $Response.config.read_only) {
[PSCustomObject]@{
Key = $Item.key
Reason = $Item.reason
Description = $Item.description
}
}
Write-Output $ObjectFinal
}
}

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

finally {
if ($Sid) {
Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
}
}
}
103 changes: 103 additions & 0 deletions PiHoleShell/Public/Config/Remove-PiHoleConfigArrayItem.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
function Remove-PiHoleConfigArrayItem {
<#
.SYNOPSIS
Delete config array item

.DESCRIPTION
Removes one item from an array-type Pi-hole configuration setting - for example, an upstream
DNS server (dns/upstreams), a local DNS record (dns/hosts), or a CNAME record
(dns/cnameRecords).

Requires your app password to have "app_sudo" enabled - Pi-hole blocks config changes from app
passwords by default. 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.

.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 Element
The array-type configuration setting to remove from, as a slash-separated path (e.g.
"dns/upstreams", "dns/hosts", or "dns/cnameRecords")

.PARAMETER Value
The exact item to remove, as it appears in the array (see Get-PiHoleConfig)

.PARAMETER Restart
Whether to restart FTL immediately if this change requires it. Defaults to $true. Set to
$false to defer the restart, e.g. when removing several items independently rather than all at
once - you'll need to restart FTL manually later for the changes to take effect

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

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

.EXAMPLE
Remove-PiHoleConfigArrayItem -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Element "dns/cnameRecords" -Value "alias.lan,target.lan"
#>
[CmdletBinding(SupportsShouldProcess = $true, HelpUri = 'https://ftl.pi-hole.net/master/docs/#delete-/config/-element-/-value-')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
param (
[Parameter(Mandatory = $true)]
[System.URI]$PiHoleServer,
[Parameter(Mandatory = $true)]
[string]$Password,
[Parameter(Mandatory = $true)]
[string]$Element,
[Parameter(Mandatory = $true)]
[string]$Value,
[Nullable[bool]]$Restart,
[bool]$IgnoreSsl = $false,
[bool]$RawOutput = $false
)
try {
$Target = "Pi-Hole config item $Value in $Element"
if ($PSCmdlet.ShouldProcess($Target, "Remove config array item")) {
$Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl

$Uri = "$($PiHoleServer.OriginalString)/api/config/$($Element.Trim('/'))/$([System.Uri]::EscapeDataString($Value))"
if ($PSBoundParameters.ContainsKey('Restart')) {
$Uri += "?restart=$($Restart.ToString().ToLower())"
}

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

$Response = Invoke-RestMethod @Params

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

else {
# A successful delete returns 204 No Content, so there's no response body to
# build a rich object from.
$Object = [PSCustomObject]@{
Element = $Element
Value = $Value
Status = "Removed"
}
Write-Output $Object
}
}
}

catch {
Write-Error -Message (ConvertTo-PiHoleFriendlyErrorMessage -ErrorRecord $_)
}

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