Initial WebhookServer implementation
Add the .NET 8 solution scaffolded against PLAN.md. Three projects share WebhookServer.Core (models, auth, execution, storage, IPC, callbacks) and WebhookServer.Service hosts an embedded Kestrel listener plus the named-pipe admin server. WebhookServer.Gui is a thin MVVM client over the pipe. Includes 25 unit tests covering HMAC verification, bearer auth, IP allowlist parsing, arg-template rendering, DPAPI round-trip, and the encrypt-on-save config store. Install/uninstall PowerShell scripts default to LocalSystem and accept a domain user or gMSA via -ServiceAccount. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Installs and starts the WebhookServer Windows Service.
|
||||
|
||||
.DESCRIPTION
|
||||
Creates the service via sc.exe pointed at the published WebhookServer.Service.exe,
|
||||
sets it to start automatically, and starts it. Re-running the script with the same
|
||||
BinaryPath updates the binary path of an existing service.
|
||||
|
||||
.PARAMETER BinaryPath
|
||||
Full path to WebhookServer.Service.exe. Defaults to .\publish\WebhookServer.Service.exe
|
||||
relative to the script.
|
||||
|
||||
.PARAMETER ServiceAccount
|
||||
Account to run the service under. Defaults to LocalSystem.
|
||||
For Active-Directory-aware hooks pass a domain user (DOMAIN\user) or a gMSA
|
||||
(DOMAIN\svc-name$ — note the trailing $). Domain users require -Password.
|
||||
Never pass LocalService — it has no network identity and cannot reach a DC.
|
||||
|
||||
.PARAMETER Password
|
||||
Password for a domain-user account. Not required for LocalSystem, NetworkService,
|
||||
LocalService, or gMSA accounts.
|
||||
|
||||
.EXAMPLE
|
||||
.\install-service.ps1 -BinaryPath C:\WebhookServer\WebhookServer.Service.exe
|
||||
|
||||
.EXAMPLE
|
||||
.\install-service.ps1 -BinaryPath C:\WebhookServer\WebhookServer.Service.exe `
|
||||
-ServiceAccount 'CONTOSO\svc-webhookserver$'
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$BinaryPath = (Join-Path $PSScriptRoot '..\publish\WebhookServer.Service.exe'),
|
||||
[string]$ServiceName = 'WebhookServer',
|
||||
[string]$DisplayName = 'Webhook Server',
|
||||
[string]$ServiceAccount = 'LocalSystem',
|
||||
[string]$Password
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if ($ServiceAccount -ieq 'LocalService') {
|
||||
throw 'LocalService has no network identity and cannot talk to a domain controller. Use LocalSystem, a domain user, or a gMSA instead.'
|
||||
}
|
||||
|
||||
$BinaryPath = (Resolve-Path -LiteralPath $BinaryPath).Path
|
||||
if (-not (Test-Path -LiteralPath $BinaryPath)) {
|
||||
throw "Binary not found: $BinaryPath"
|
||||
}
|
||||
|
||||
# Build sc.exe argv. Note: sc.exe is fussy about spaces — keep "key= value" format.
|
||||
$obj = $ServiceAccount
|
||||
$existing = sc.exe query $ServiceName 2>$null
|
||||
|
||||
if ($existing) {
|
||||
Write-Host "Service '$ServiceName' already exists; updating binPath and account."
|
||||
sc.exe config $ServiceName binPath= "`"$BinaryPath`"" obj= $obj $(if ($Password) { "password= $Password" }) | Out-Null
|
||||
} else {
|
||||
$args = @(
|
||||
'create', $ServiceName,
|
||||
"binPath=", "`"$BinaryPath`"",
|
||||
"DisplayName=", "`"$DisplayName`"",
|
||||
"start=", "auto",
|
||||
"obj=", $obj
|
||||
)
|
||||
if ($Password) { $args += @('password=', $Password) }
|
||||
sc.exe @args | Out-Null
|
||||
}
|
||||
|
||||
# Configure failure recovery: restart the service on first/second failure, reset count after a day.
|
||||
sc.exe failure $ServiceName reset= 86400 actions= restart/5000/restart/5000/restart/5000 | Out-Null
|
||||
|
||||
Write-Host "Starting service '$ServiceName'..."
|
||||
sc.exe start $ServiceName | Out-Null
|
||||
|
||||
Start-Sleep -Seconds 1
|
||||
sc.exe query $ServiceName
|
||||
@@ -0,0 +1,38 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Stops and removes the WebhookServer Windows Service.
|
||||
|
||||
.DESCRIPTION
|
||||
Leaves C:\ProgramData\WebhookServer (config + logs) untouched. Pass -PurgeData
|
||||
to remove that directory as well.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ServiceName = 'WebhookServer',
|
||||
[switch]$PurgeData
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$existing = sc.exe query $ServiceName 2>$null
|
||||
if (-not $existing) {
|
||||
Write-Host "Service '$ServiceName' is not installed."
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "Stopping service '$ServiceName'..."
|
||||
sc.exe stop $ServiceName 2>$null | Out-Null
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
Write-Host "Deleting service '$ServiceName'..."
|
||||
sc.exe delete $ServiceName | Out-Null
|
||||
|
||||
if ($PurgeData) {
|
||||
$dataRoot = Join-Path $env:ProgramData 'WebhookServer'
|
||||
if (Test-Path -LiteralPath $dataRoot) {
|
||||
Write-Host "Removing $dataRoot"
|
||||
Remove-Item -LiteralPath $dataRoot -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host 'Done.'
|
||||
Reference in New Issue
Block a user