diff --git a/PiHoleShell/PiHoleShell.psm1 b/PiHoleShell/PiHoleShell.psm1 index a28485d..6f5330e 100644 --- a/PiHoleShell/PiHoleShell.psm1 +++ b/PiHoleShell/PiHoleShell.psm1 @@ -34,7 +34,9 @@ Export-ModuleMember -Function @( 'Get-PiHoleList', 'Search-PiHoleListDomain', 'Add-PiHoleList', 'Remove-PiHoleList', 'Update-PiHoleList', ` #FTLInformation 'Get-PiHoleInfoMessage', 'Get-PiHoleInfoHost', 'Get-PiHoleInfoClient', 'Get-PiHoleInfoLogin', 'Get-PiHoleInfoSystem', 'Get-PiHoleInfoFtl', ` - 'Get-PiHoleInfoSensors', 'Get-PiHoleInfoDatabase', 'Get-PiHoleInfoVersion', 'Get-PiHoleInfoMetrics', 'Get-PiHoleInfoMessageCount', 'Remove-PiHoleInfoMessage', ` + 'Get-PiHoleInfoSensors', 'Get-PiHoleInfoDatabase', 'Get-PiHoleInfoVersion', 'Get-PiHoleInfoMetrics', 'Get-PiHoleInfoMessageCount', 'Remove-PiHoleInfoMessage', 'Get-PiHoleLogWebserver', ` #History - 'Get-PiHoleHistory', 'Get-PiHoleHistoryDatabase', 'Get-PiHoleHistoryClient', 'Get-PiHoleHistoryDatabaseClient' + 'Get-PiHoleHistory', 'Get-PiHoleHistoryDatabase', 'Get-PiHoleHistoryClient', 'Get-PiHoleHistoryDatabaseClient', ` + #Teleporter + 'Get-PiHoleTeleporterDownload' ) \ No newline at end of file diff --git a/PiHoleShell/Public/FTLInformation/Get-PiHoleLogWebserver.ps1 b/PiHoleShell/Public/FTLInformation/Get-PiHoleLogWebserver.ps1 index 845b89d..feaff04 100644 --- a/PiHoleShell/Public/FTLInformation/Get-PiHoleLogWebserver.ps1 +++ b/PiHoleShell/Public/FTLInformation/Get-PiHoleLogWebserver.ps1 @@ -1,11 +1,32 @@ - function Get-PiHoleLogWebserver { <# .SYNOPSIS -Get info about logs for webserver +Get webserver log content + +.DESCRIPTION +Request content from the log of the embedded CivetWeb HTTP server. Every response includes a +NextID; pass it back as -NextID on your next call to only get lines added since then, making +periodic polling for new log lines easy without checking for duplicates. + +.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 NextID +Only return log lines added after this ID (returned as NextID on a previous call). Omit to +get the full available log + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Get-PiHoleLogWebserver -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" #> - #Work In Progress [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/logs/webserver')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] param ( @@ -13,19 +34,16 @@ Get info about logs for webserver [System.URI]$PiHoleServer, [Parameter(Mandatory = $true)] [string]$Password, - [int]$NextID, + [Nullable[int]]$NextID, [bool]$IgnoreSsl = $false, [bool]$RawOutput = $false ) try { $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl - if ($NextID) { - $Uri = "$($PiHoleServer.OriginalString)/api/logs/webserver?nextId=$NextId" - - } - else { - $Uri = "$($PiHoleServer.OriginalString)/api/logs/webserver" + $Uri = "$($PiHoleServer.OriginalString)/api/logs/webserver" + if ($PSBoundParameters.ContainsKey('NextID')) { + $Uri += "?nextID=$NextID" } $Params = @{ @@ -43,13 +61,26 @@ Get info about logs for webserver } else { - #$ObjectFinal = @() + $Log = foreach ($Item in $Response.log) { + [PSCustomObject]@{ + Timestamp = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.timestamp).LocalTime + Message = $Item.message + Priority = $Item.prio + } + } + + $Object = [PSCustomObject]@{ + Log = $Log + NextID = $Response.nextID + Pid = $Response.pid + File = $Response.file + } + Write-Output $Object } } catch { Write-Error -Message $_.Exception.Message - break } finally { @@ -57,4 +88,4 @@ Get info about logs for webserver Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl } } -} \ No newline at end of file +} diff --git a/PiHoleShell/Public/ListManagement/Get-PiHoleList.ps1 b/PiHoleShell/Public/ListManagement/Get-PiHoleList.ps1 index 50d09ba..70d58f8 100644 --- a/PiHoleShell/Public/ListManagement/Get-PiHoleList.ps1 +++ b/PiHoleShell/Public/ListManagement/Get-PiHoleList.ps1 @@ -3,20 +3,31 @@ function Get-PiHoleList { .SYNOPSIS Get lists +.DESCRIPTION +Request Pi-hole's subscribed allow/block lists. Omit -List to get every list; 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 List +The URL of a specific list to return. Omit to return every list + .PARAMETER IgnoreSsl Set to $true to skip SSL certificate validation .PARAMETER RawOutput This will dump the response instead of the formatted object +.EXAMPLE +Get-PiHoleList -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" + +.EXAMPLE +Get-PiHoleList -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -List "https://hosts-file.net/ad_servers.txt" #> - #Work In Progress [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/lists/-list-')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] param ( diff --git a/PiHoleShell/Public/Teleporter/Get-PiHoleTeleporterDownload.ps1 b/PiHoleShell/Public/Teleporter/Get-PiHoleTeleporterDownload.ps1 index 4da0524..07fc347 100644 --- a/PiHoleShell/Public/Teleporter/Get-PiHoleTeleporterDownload.ps1 +++ b/PiHoleShell/Public/Teleporter/Get-PiHoleTeleporterDownload.ps1 @@ -1,11 +1,32 @@ function Get-PiHoleTeleporterDownload { <# .SYNOPSIS -Get info about logs for webserver +Export Pi-hole settings +.DESCRIPTION +Downloads an archived copy of Pi-hole's current configuration (a Teleporter backup) to disk. +The API always returns a binary application/zip archive, not JSON, so there's no -RawOutput +option here - the downloaded file is the only output. + +.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 FolderPath +The folder to save the backup file into. Must already exist + +.PARAMETER FileName +The name to give the backup file, without an extension - ".zip" is appended automatically + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.EXAMPLE +Get-PiHoleTeleporterDownload -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -FolderPath "C:\Backups" -FileName "pihole-backup" #> - #Work In Progress - [CmdletBinding()] + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/teleporter')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] param ( [Parameter(Mandatory = $true)] @@ -16,51 +37,41 @@ Get info about logs for webserver [System.IO.DirectoryInfo]$FolderPath, [Parameter(Mandatory = $true)] [string]$FileName, - [bool]$IgnoreSsl = $false, - [bool]$RawOutput = $false + [bool]$IgnoreSsl = $false ) try { - $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl - if (!(Test-Path -Path $FolderPath)) { throw "$FolderPath does not exist!" } - $FileName = "$FileName.tar.gz" + $FileName = "$FileName.zip" $OutFile = "$FolderPath\$FileName" if (Test-Path -Path $OutFile) { throw "$OutFile already exists!" } + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + $Params = @{ Headers = @{sid = $($Sid) } Uri = "$($PiHoleServer.OriginalString)/api/teleporter" Method = "Get" SkipCertificateCheck = $IgnoreSsl - ContentType = "application/json" + OutFile = $OutFile } - $Response = Invoke-RestMethod @Params -OutFile $OutFile - - if ($RawOutput) { - Write-Output $Response - } + Invoke-RestMethod @Params - else { - $ObjectFinal = @() - $Object = [PSCustomObject]@{ - FileName = $FileName - FilePath = $OutFile - RootFolder = $FolderPath - FileSizeKB = [math]::Ceiling((Get-Item $OutFile).Length / 1KB) - } - $ObjectFinal += $Object - Write-Output $ObjectFinal + $Object = [PSCustomObject]@{ + FileName = $FileName + FilePath = $OutFile + RootFolder = $FolderPath + FileSizeKB = [math]::Ceiling((Get-Item $OutFile).Length / 1KB) } + Write-Output $Object } catch { Write-Error -Message $_.Exception.Message - break } finally { @@ -68,4 +79,4 @@ Get info about logs for webserver Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl } } -} \ No newline at end of file +} diff --git a/README.md b/README.md index b4711ea..181b9b9 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Functions marked 🚧 are still under active development — signatures and outp | Function | Description | |---|---| | `Add-PiHoleList` | Add a new list | -| `Get-PiHoleList` 🚧 | Get lists | +| `Get-PiHoleList` | Get lists | | `Remove-PiHoleList` | Remove a list | | `Search-PiHoleListDomain` | _No description yet_ | | `Update-PiHoleList` | Update a list | @@ -155,7 +155,9 @@ Functions marked 🚧 are still under active development — signatures and outp | `Get-PiHoleInfoSensors` | Get info about various sensors | | `Get-PiHoleInfoSystem` | Get info about various system parameters | | `Get-PiHoleInfoVersion` | Get Pi-hole version | +| `Get-PiHoleLogWebserver` | Get webserver log content | | `Get-PiHolePadd` | Get summarized data for PADD | +| `Get-PiHoleTeleporterDownload` | Export Pi-hole settings | | `Remove-PiHoleInfoMessage` | Delete a Pi-hole diagnosis message | ### Authentication diff --git a/tests/FTLInformation/Get-PiHoleLogWebserver.Integration.Tests.ps1 b/tests/FTLInformation/Get-PiHoleLogWebserver.Integration.Tests.ps1 new file mode 100644 index 0000000..05e5d71 --- /dev/null +++ b/tests/FTLInformation/Get-PiHoleLogWebserver.Integration.Tests.ps1 @@ -0,0 +1,55 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Get-PiHoleLogWebserver (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + It 'returns webserver log content as a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleLogWebserver -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.NextID | Should -BeGreaterOrEqual 0 + $result.File | Should -Not -BeNullOrEmpty + } + + It 'only returns lines added after NextID on a follow-up call' -Skip:(-not $script:ConfigAvailable) { + $first = Get-PiHoleLogWebserver -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + + $second = Get-PiHoleLogWebserver -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -NextID $first.NextID + $second | Format-List | Out-String | Write-Host + + # No guarantee new webserver log lines were generated between the two calls, so this + # just confirms the call succeeds and NextID never goes backwards. + $second.NextID | Should -BeGreaterOrEqual $first.NextID + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleLogWebserver -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result.PSObject.Properties.Name | Should -Contain 'log' + $result.PSObject.Properties.Name | Should -Contain 'nextID' + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleLogWebserver -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Teleporter/Get-PiHoleTeleporterDownload.Integration.Tests.ps1 b/tests/Teleporter/Get-PiHoleTeleporterDownload.Integration.Tests.ps1 new file mode 100644 index 0000000..730f051 --- /dev/null +++ b/tests/Teleporter/Get-PiHoleTeleporterDownload.Integration.Tests.ps1 @@ -0,0 +1,72 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Get-PiHoleTeleporterDownload (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + + $script:TestFolder = Join-Path ([System.IO.Path]::GetTempPath()) "PiHoleShellPesterTeleporter" + if (Test-Path $script:TestFolder) { + Remove-Item -Path $script:TestFolder -Recurse -Force + } + New-Item -ItemType Directory -Path $script:TestFolder | Out-Null + } + + AfterAll { + if (Test-Path $script:TestFolder) { + Remove-Item -Path $script:TestFolder -Recurse -Force + } + } + + It 'downloads a real ZIP backup and returns a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleTeleporterDownload -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -FolderPath $script:TestFolder -FileName 'pester-backup' + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.FileName | Should -Be 'pester-backup.zip' + $result.FileSizeKB | Should -BeGreaterThan 0 + Test-Path $result.FilePath | Should -BeTrue + + # Confirm it's a genuine, openable ZIP archive, not just a file with a .zip name. + $zip = [System.IO.Compression.ZipFile]::OpenRead($result.FilePath) + try { + $zip.Entries.Count | Should -BeGreaterThan 0 + } + finally { + $zip.Dispose() + } + } + + It 'errors when the destination file already exists' -Skip:(-not $script:ConfigAvailable) { + Get-PiHoleTeleporterDownload -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -FolderPath $script:TestFolder -FileName 'duplicate-backup' | Out-Null + + $result = Get-PiHoleTeleporterDownload -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -FolderPath $script:TestFolder -FileName 'duplicate-backup' -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } + + It 'errors when the destination folder does not exist' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleTeleporterDownload -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -FolderPath (Join-Path $script:TestFolder 'does-not-exist') -FileName 'x' -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleTeleporterDownload -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -FolderPath $script:TestFolder -FileName 'bad-password-backup' -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +}