a1a17e81a1
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
603 lines
20 KiB
PowerShell
603 lines
20 KiB
PowerShell
###############################################################################
|
|
#### windows-iis-smoke-tests.ps1 — Verify IIS health ####
|
|
#### Checks W3SVC, WAS, app pools, site responses, SSL certs, ####
|
|
#### bindings, ARR, URL Rewrite, logs, FREB, and event log. ####
|
|
#### ####
|
|
#### Author: Phil Connor ####
|
|
#### Contact: contact@mylinux.work ####
|
|
#### License: MIT ####
|
|
#### Version: 1.0 ####
|
|
#### ####
|
|
#### Usage: .\windows-iis-smoke-tests.ps1 ####
|
|
#### .\windows-iis-smoke-tests.ps1 -SiteNames "Default Web Site" ####
|
|
#### .\windows-iis-smoke-tests.ps1 -OutputFormat tap ####
|
|
#### ####
|
|
#### See -Help for all options. ####
|
|
###############################################################################
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[string[]]$SiteNames = @(),
|
|
[int]$CertExpiryDays = 30,
|
|
[string]$TestUrl = "",
|
|
[ValidateSet("text","tap")]
|
|
[string]$OutputFormat = "text",
|
|
[switch]$NoColor,
|
|
[switch]$Help
|
|
)
|
|
|
|
$ErrorActionPreference = "Continue"
|
|
|
|
# ============================================================================
|
|
# HELP
|
|
# ============================================================================
|
|
|
|
if ($Help) {
|
|
@"
|
|
Usage: .\windows-iis-smoke-tests.ps1 [OPTIONS]
|
|
|
|
Smoke-test IIS (Internet Information Services). PowerShell 5.1+.
|
|
Requires the WebAdministration module (IIS Management Tools).
|
|
|
|
Parameters:
|
|
-SiteNames "Site1","Site2" Sites to test (default: all sites)
|
|
-CertExpiryDays N Warn if cert expires within N days (default: 30)
|
|
-TestUrl URL Optional specific URL to test
|
|
-OutputFormat FORMAT Output: text (default), tap
|
|
-NoColor Disable coloured output
|
|
-Verbose Show debug output
|
|
-Help Show this help
|
|
|
|
Examples:
|
|
.\windows-iis-smoke-tests.ps1
|
|
.\windows-iis-smoke-tests.ps1 -SiteNames "Default Web Site","IntranetApp"
|
|
.\windows-iis-smoke-tests.ps1 -CertExpiryDays 60 -OutputFormat tap
|
|
.\windows-iis-smoke-tests.ps1 -TestUrl "https://intranet.corp.local/health"
|
|
.\windows-iis-smoke-tests.ps1 -NoColor
|
|
"@
|
|
exit 0
|
|
}
|
|
|
|
# ============================================================================
|
|
# STATE
|
|
# ============================================================================
|
|
|
|
$script:Pass = 0
|
|
$script:Fail = 0
|
|
$script:Skip = 0
|
|
$script:Total = 0
|
|
$script:Results = @()
|
|
$script:StartTime = Get-Date
|
|
|
|
# ============================================================================
|
|
# COLORS
|
|
# ============================================================================
|
|
|
|
function Write-Color {
|
|
param([string]$Text, [string]$Color = "White")
|
|
if ($NoColor) {
|
|
Write-Host $Text
|
|
} else {
|
|
Write-Host $Text -ForegroundColor $Color
|
|
}
|
|
}
|
|
|
|
function Write-Log { param([string]$Msg) Write-Color "[INFO] $Msg" "Cyan" }
|
|
function Write-Warn { param([string]$Msg) Write-Color "[WARN] $Msg" "Yellow" }
|
|
function Write-Err { param([string]$Msg) Write-Color "[ERROR] $Msg" "Red" }
|
|
|
|
# ============================================================================
|
|
# TEST RESULT RECORDING
|
|
# ============================================================================
|
|
|
|
function Record-Pass {
|
|
param([string]$Name, [string]$Detail = "")
|
|
$script:Pass++
|
|
$script:Total++
|
|
$script:Results += [PSCustomObject]@{ Status="PASS"; Name=$Name; Detail=$Detail }
|
|
if ($OutputFormat -eq "tap") {
|
|
Write-Host "ok $($script:Total) - $Name$(if($Detail){" ($Detail)"})"
|
|
} else {
|
|
$mark = if ($NoColor) { "[PASS]" } else { [char]0x2713 }
|
|
$msg = " $mark $Name"
|
|
if ($Detail) { $msg += " - $Detail" }
|
|
Write-Color $msg "Green"
|
|
}
|
|
}
|
|
|
|
function Record-Fail {
|
|
param([string]$Name, [string]$Detail = "")
|
|
$script:Fail++
|
|
$script:Total++
|
|
$script:Results += [PSCustomObject]@{ Status="FAIL"; Name=$Name; Detail=$Detail }
|
|
if ($OutputFormat -eq "tap") {
|
|
Write-Host "not ok $($script:Total) - $Name"
|
|
if ($Detail) { Write-Host " # $Detail" }
|
|
} else {
|
|
$mark = if ($NoColor) { "[FAIL]" } else { [char]0x2717 }
|
|
$msg = " $mark $Name"
|
|
if ($Detail) { $msg += " - $Detail" }
|
|
Write-Color $msg "Red"
|
|
}
|
|
}
|
|
|
|
function Record-Skip {
|
|
param([string]$Name, [string]$Reason = "")
|
|
$script:Skip++
|
|
$script:Total++
|
|
$script:Results += [PSCustomObject]@{ Status="SKIP"; Name=$Name; Detail=$Reason }
|
|
if ($OutputFormat -eq "tap") {
|
|
Write-Host "ok $($script:Total) - $Name # SKIP $Reason"
|
|
} else {
|
|
$mark = if ($NoColor) { "[SKIP]" } else { [char]0x2298 }
|
|
$msg = " $mark $Name"
|
|
if ($Reason) { $msg += " - $Reason" }
|
|
Write-Color $msg "Yellow"
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# HELPERS
|
|
# ============================================================================
|
|
|
|
function Write-Section {
|
|
param([string]$Name)
|
|
if ($OutputFormat -eq "text") {
|
|
Write-Host ""
|
|
Write-Color $Name "White"
|
|
}
|
|
}
|
|
|
|
function Get-TargetSites {
|
|
if ($SiteNames.Count -gt 0) {
|
|
return @(Get-Website | Where-Object { $SiteNames -contains $_.Name })
|
|
}
|
|
return @(Get-Website)
|
|
}
|
|
|
|
# ============================================================================
|
|
# TESTS
|
|
# ============================================================================
|
|
|
|
# -- 1. W3SVC Service -------------------------------------------------------
|
|
|
|
function Test-W3SVC {
|
|
Write-Section "Services"
|
|
|
|
try {
|
|
$svc = Get-Service -Name W3SVC -ErrorAction Stop
|
|
if ($svc.Status -eq "Running") {
|
|
Record-Pass "W3SVC service running"
|
|
} else {
|
|
Record-Fail "W3SVC service running" "status: $($svc.Status)"
|
|
}
|
|
} catch {
|
|
Record-Fail "W3SVC service running" "service not found"
|
|
}
|
|
}
|
|
|
|
# -- 2. WAS Service ----------------------------------------------------------
|
|
|
|
function Test-WAS {
|
|
try {
|
|
$svc = Get-Service -Name WAS -ErrorAction Stop
|
|
if ($svc.Status -eq "Running") {
|
|
Record-Pass "WAS service running"
|
|
} else {
|
|
Record-Fail "WAS service running" "status: $($svc.Status)"
|
|
}
|
|
} catch {
|
|
Record-Fail "WAS service running" "service not found"
|
|
}
|
|
}
|
|
|
|
# -- 3. Application Pool States ---------------------------------------------
|
|
|
|
function Test-AppPoolStates {
|
|
Write-Section "Application Pools"
|
|
|
|
try {
|
|
$pools = @(Get-WebAppPoolState -ErrorAction Stop)
|
|
if ($pools.Count -eq 0) {
|
|
Record-Skip "application pools" "no pools found"
|
|
return
|
|
}
|
|
|
|
$started = @($pools | Where-Object { $_.Value -eq "Started" })
|
|
$stopped = @($pools | Where-Object { $_.Value -ne "Started" })
|
|
|
|
foreach ($p in $started) {
|
|
$poolName = ($p.ItemXPath -replace ".*@name='([^']+)'.*", '$1')
|
|
Record-Pass "app pool started" $poolName
|
|
}
|
|
|
|
foreach ($p in $stopped) {
|
|
$poolName = ($p.ItemXPath -replace ".*@name='([^']+)'.*", '$1')
|
|
Record-Fail "app pool stopped" "$poolName state: $($p.Value)"
|
|
}
|
|
} catch {
|
|
Record-Fail "application pools" $_.Exception.Message
|
|
}
|
|
}
|
|
|
|
# -- 4. Site HTTP Responses --------------------------------------------------
|
|
|
|
function Test-SiteResponses {
|
|
Write-Section "Site Responses"
|
|
|
|
$sites = Get-TargetSites
|
|
if ($sites.Count -eq 0) {
|
|
Record-Skip "site responses" "no sites found"
|
|
return
|
|
}
|
|
|
|
foreach ($site in $sites) {
|
|
if ($site.State -ne "Started") {
|
|
Record-Fail "site responds ($($site.Name))" "site not started"
|
|
continue
|
|
}
|
|
|
|
$bindings = @($site.Bindings.Collection)
|
|
if ($bindings.Count -eq 0) {
|
|
Record-Skip "site responds ($($site.Name))" "no bindings"
|
|
continue
|
|
}
|
|
|
|
$binding = $bindings[0]
|
|
$proto = $binding.protocol
|
|
$info = $binding.bindingInformation -split ":"
|
|
$port = if ($info.Count -ge 2) { $info[1] } else { if ($proto -eq "https") { "443" } else { "80" } }
|
|
$host_ = if ($info.Count -ge 3 -and $info[2]) { $info[2] } else { "localhost" }
|
|
|
|
$url = "${proto}://${host_}:${port}/"
|
|
|
|
try {
|
|
$resp = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop
|
|
Record-Pass "site responds ($($site.Name))" "$url HTTP $($resp.StatusCode)"
|
|
} catch {
|
|
$code = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { "timeout" }
|
|
if ($code -ge 200 -and $code -lt 500) {
|
|
Record-Pass "site responds ($($site.Name))" "$url HTTP $code"
|
|
} else {
|
|
Record-Fail "site responds ($($site.Name))" "$url HTTP $code"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# -- 5. SSL Certificate Validation ------------------------------------------
|
|
|
|
function Test-SSLCerts {
|
|
Write-Section "SSL Certificates"
|
|
|
|
$sites = Get-TargetSites
|
|
$httpsBindings = @()
|
|
|
|
foreach ($site in $sites) {
|
|
foreach ($b in $site.Bindings.Collection) {
|
|
if ($b.protocol -eq "https" -and $b.certificateHash) {
|
|
$httpsBindings += [PSCustomObject]@{
|
|
SiteName = $site.Name
|
|
Hash = $b.certificateHash
|
|
Store = if ($b.certificateStoreName) { $b.certificateStoreName } else { "My" }
|
|
Binding = $b.bindingInformation
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($httpsBindings.Count -eq 0) {
|
|
Record-Skip "SSL certificates" "no HTTPS bindings found"
|
|
return
|
|
}
|
|
|
|
$checked = @{}
|
|
foreach ($hb in $httpsBindings) {
|
|
if ($checked.ContainsKey($hb.Hash)) { continue }
|
|
$checked[$hb.Hash] = $true
|
|
|
|
try {
|
|
$cert = Get-ChildItem "Cert:\LocalMachine\$($hb.Store)\$($hb.Hash)" -ErrorAction Stop
|
|
$daysLeft = [math]::Floor(($cert.NotAfter - (Get-Date)).TotalDays)
|
|
$cn = $cert.Subject -replace "CN=", "" -replace ",.*", ""
|
|
|
|
if ($cert.NotAfter -lt (Get-Date)) {
|
|
Record-Fail "SSL certificate ($cn)" "EXPIRED on $($cert.NotAfter.ToString('yyyy-MM-dd'))"
|
|
} elseif ($daysLeft -lt $CertExpiryDays) {
|
|
Record-Fail "certificate expiry ($cn)" "$daysLeft days remaining (threshold: $CertExpiryDays)"
|
|
} else {
|
|
Record-Pass "SSL certificate valid ($cn)" "expires $($cert.NotAfter.ToString('yyyy-MM-dd')), $daysLeft days"
|
|
}
|
|
} catch {
|
|
Record-Fail "SSL certificate ($($hb.SiteName))" "cert hash $($hb.Hash) not found in $($hb.Store) store"
|
|
}
|
|
}
|
|
}
|
|
|
|
# -- 6. Site Bindings --------------------------------------------------------
|
|
|
|
function Test-SiteBindings {
|
|
Write-Section "Configuration"
|
|
|
|
$sites = Get-TargetSites
|
|
$totalBindings = 0
|
|
$validBindings = 0
|
|
|
|
foreach ($site in $sites) {
|
|
foreach ($b in $site.Bindings.Collection) {
|
|
$totalBindings++
|
|
$info = $b.bindingInformation
|
|
$proto = $b.protocol
|
|
if ($proto -in @("http","https") -and $info -match "^\*?:?\d+:") {
|
|
$validBindings++
|
|
} elseif ($proto -in @("net.tcp","net.pipe","net.msmq","msmq.formatname")) {
|
|
$validBindings++
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($totalBindings -eq 0) {
|
|
Record-Skip "site bindings" "no bindings configured"
|
|
} elseif ($validBindings -eq $totalBindings) {
|
|
Record-Pass "site bindings valid" "$($sites.Count) sites, $totalBindings bindings"
|
|
} else {
|
|
$invalid = $totalBindings - $validBindings
|
|
Record-Fail "site bindings" "$invalid invalid out of $totalBindings bindings"
|
|
}
|
|
}
|
|
|
|
# -- 7. Default Document ----------------------------------------------------
|
|
|
|
function Test-DefaultDocument {
|
|
$sites = Get-TargetSites
|
|
if ($sites.Count -eq 0) { return }
|
|
|
|
foreach ($site in $sites) {
|
|
try {
|
|
$docs = Get-WebConfiguration -Filter "system.webServer/defaultDocument/files/add" -PSPath "IIS:\Sites\$($site.Name)" -ErrorAction Stop
|
|
if ($docs -and @($docs).Count -gt 0) {
|
|
$first = @($docs)[0].value
|
|
Record-Pass "default document ($($site.Name))" $first
|
|
} else {
|
|
Record-Fail "default document ($($site.Name))" "none configured"
|
|
}
|
|
} catch {
|
|
Record-Skip "default document ($($site.Name))" "could not read config"
|
|
}
|
|
}
|
|
}
|
|
|
|
# -- 8. Custom URL Test -----------------------------------------------------
|
|
|
|
function Test-CustomUrl {
|
|
if (-not $TestUrl) { return }
|
|
|
|
Write-Section "Custom URL"
|
|
|
|
try {
|
|
$resp = Invoke-WebRequest -Uri $TestUrl -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop
|
|
Record-Pass "custom URL" "$TestUrl HTTP $($resp.StatusCode)"
|
|
} catch {
|
|
$code = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { "timeout" }
|
|
Record-Fail "custom URL" "$TestUrl HTTP $code"
|
|
}
|
|
}
|
|
|
|
# -- 9. ARR Health -----------------------------------------------------------
|
|
|
|
function Test-ARRHealth {
|
|
Write-Section "Modules"
|
|
|
|
try {
|
|
$arr = Get-WebGlobalModule -Name "ApplicationRequestRouting" -ErrorAction Stop
|
|
if (-not $arr) {
|
|
Record-Skip "ARR health" "ARR not installed"
|
|
return
|
|
}
|
|
} catch {
|
|
Record-Skip "ARR health" "ARR not installed"
|
|
return
|
|
}
|
|
|
|
try {
|
|
$farms = Get-WebConfiguration -Filter "webFarms/webFarm" -PSPath "MACHINE/WEBROOT/APPHOST" -ErrorAction Stop
|
|
if (-not $farms -or @($farms).Count -eq 0) {
|
|
Record-Pass "ARR health" "ARR installed, no server farms configured"
|
|
return
|
|
}
|
|
|
|
foreach ($farm in @($farms)) {
|
|
$farmName = $farm.name
|
|
$servers = @($farm.Collection)
|
|
$healthy = @($servers | Where-Object { $_.enabled -eq $true })
|
|
if ($healthy.Count -eq $servers.Count) {
|
|
Record-Pass "ARR farm ($farmName)" "$($servers.Count) servers enabled"
|
|
} else {
|
|
$disabled = $servers.Count - $healthy.Count
|
|
Record-Fail "ARR farm ($farmName)" "$disabled of $($servers.Count) servers disabled"
|
|
}
|
|
}
|
|
} catch {
|
|
Record-Fail "ARR health" $_.Exception.Message
|
|
}
|
|
}
|
|
|
|
# -- 10. URL Rewrite Module -------------------------------------------------
|
|
|
|
function Test-URLRewrite {
|
|
try {
|
|
$mod = Get-WebGlobalModule -Name "RewriteModule" -ErrorAction Stop
|
|
if ($mod) {
|
|
Record-Pass "URL Rewrite module installed"
|
|
} else {
|
|
Record-Skip "URL Rewrite module" "not installed"
|
|
}
|
|
} catch {
|
|
Record-Skip "URL Rewrite module" "not installed"
|
|
}
|
|
}
|
|
|
|
# -- 11. Log Directory -------------------------------------------------------
|
|
|
|
function Test-LogDirectory {
|
|
Write-Section "Logging"
|
|
|
|
try {
|
|
$logDir = (Get-WebConfigurationProperty -Filter "system.applicationHost/sites/siteDefaults/logFile" -Name "directory" -ErrorAction Stop).Value
|
|
$expanded = [System.Environment]::ExpandEnvironmentVariables($logDir)
|
|
|
|
if (Test-Path $expanded) {
|
|
try {
|
|
$testFile = Join-Path $expanded ".iis-smoke-test-$(Get-Random).tmp"
|
|
[IO.File]::WriteAllText($testFile, "test")
|
|
Remove-Item $testFile -Force
|
|
Record-Pass "log directory accessible" $expanded
|
|
} catch {
|
|
Record-Fail "log directory writable" "$expanded - not writable"
|
|
}
|
|
} else {
|
|
Record-Fail "log directory exists" "$expanded - not found"
|
|
}
|
|
} catch {
|
|
Record-Skip "log directory" "could not read log configuration"
|
|
}
|
|
}
|
|
|
|
# -- 12. Failed Request Tracing ----------------------------------------------
|
|
|
|
function Test-FREB {
|
|
$sites = Get-TargetSites
|
|
$frebFound = $false
|
|
|
|
foreach ($site in $sites) {
|
|
try {
|
|
$tracing = Get-WebConfiguration -Filter "system.webServer/tracing/traceFailedRequests" -PSPath "IIS:\Sites\$($site.Name)" -ErrorAction Stop
|
|
if ($tracing -and @($tracing).Count -gt 0) {
|
|
$frebFound = $true
|
|
$frebDir = "$($site.TraceFailedRequestsLogging.Directory)"
|
|
if ($frebDir -and (Test-Path ([System.Environment]::ExpandEnvironmentVariables($frebDir)))) {
|
|
Record-Pass "Failed Request Tracing ($($site.Name))" "enabled, logs accessible"
|
|
} else {
|
|
Record-Pass "Failed Request Tracing ($($site.Name))" "enabled"
|
|
}
|
|
}
|
|
} catch { }
|
|
}
|
|
|
|
if (-not $frebFound) {
|
|
Record-Skip "Failed Request Tracing" "FREB not enabled on any site"
|
|
}
|
|
}
|
|
|
|
# -- 13. Event Log -----------------------------------------------------------
|
|
|
|
function Test-EventLog {
|
|
Write-Section "Event Log"
|
|
|
|
try {
|
|
$events = @()
|
|
$sources = @("IIS", "W3SVC", "WAS", "Microsoft-Windows-IIS*")
|
|
foreach ($src in $sources) {
|
|
try {
|
|
$filter = @{
|
|
LogName = "System","Application"
|
|
Level = 2
|
|
StartTime = (Get-Date).AddHours(-24)
|
|
}
|
|
$found = @(Get-WinEvent -FilterHashtable $filter -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.ProviderName -like "*IIS*" -or $_.ProviderName -like "W3SVC" -or $_.ProviderName -like "WAS" })
|
|
$events += $found
|
|
} catch { }
|
|
}
|
|
|
|
if ($events.Count -eq 0) {
|
|
Record-Pass "event log clean" "0 IIS errors in last 24h"
|
|
} else {
|
|
$latest = $events[0]
|
|
$msg = $latest.Message.Substring(0, [math]::Min(80, $latest.Message.Length))
|
|
Record-Fail "event log" "$($events.Count) IIS error(s) in 24h — latest: $msg"
|
|
}
|
|
} catch {
|
|
if ($_.Exception.Message -match "No events were found") {
|
|
Record-Pass "event log clean" "0 IIS errors in last 24h"
|
|
} else {
|
|
Record-Fail "event log" $_.Exception.Message
|
|
}
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# OUTPUT
|
|
# ============================================================================
|
|
|
|
function Write-Header {
|
|
if ($OutputFormat -eq "tap") {
|
|
Write-Host "TAP version 13"
|
|
} else {
|
|
Write-Host ""
|
|
Write-Color "Windows IIS Smoke Tests" "White"
|
|
Write-Host "Server: $env:COMPUTERNAME"
|
|
Write-Host "Time: $(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ')"
|
|
}
|
|
}
|
|
|
|
function Write-Summary {
|
|
$duration = [math]::Floor(((Get-Date) - $script:StartTime).TotalSeconds)
|
|
|
|
if ($OutputFormat -eq "tap") {
|
|
Write-Host "1..$($script:Total)"
|
|
Write-Host "# pass $($script:Pass)"
|
|
Write-Host "# fail $($script:Fail)"
|
|
Write-Host "# skip $($script:Skip)"
|
|
} else {
|
|
Write-Host ""
|
|
$separator = [string]::new([char]0x2500, 40)
|
|
Write-Color $separator "White"
|
|
Write-Color "Summary $env:COMPUTERNAME" "White"
|
|
|
|
$summaryLine = " $($script:Pass) passed $($script:Fail) failed $($script:Skip) skipped (${duration}s)"
|
|
Write-Host $summaryLine
|
|
Write-Color $separator "White"
|
|
|
|
if ($script:Fail -eq 0) {
|
|
Write-Color "All tests passed." "Green"
|
|
} else {
|
|
Write-Color "$($script:Fail) test(s) failed." "Red"
|
|
}
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# MAIN
|
|
# ============================================================================
|
|
|
|
Write-Header
|
|
|
|
# Load WebAdministration module
|
|
try {
|
|
Import-Module WebAdministration -ErrorAction Stop
|
|
Write-Verbose "WebAdministration module loaded"
|
|
} catch {
|
|
Write-Err "WebAdministration module not available. Install IIS Management Tools."
|
|
exit 1
|
|
}
|
|
|
|
# Run all tests
|
|
Test-W3SVC
|
|
Test-WAS
|
|
Test-AppPoolStates
|
|
Test-SiteResponses
|
|
Test-SSLCerts
|
|
Test-SiteBindings
|
|
Test-DefaultDocument
|
|
Test-CustomUrl
|
|
Test-ARRHealth
|
|
Test-URLRewrite
|
|
Test-LogDirectory
|
|
Test-FREB
|
|
Test-EventLog
|
|
|
|
Write-Summary
|
|
|
|
if ($script:Fail -eq 0) { exit 0 } else { exit 1 }
|