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,6 @@
|
||||
namespace WebhookServer.Core.Models;
|
||||
|
||||
public sealed class BearerOptions
|
||||
{
|
||||
public ProtectedString Secret { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace WebhookServer.Core.Models;
|
||||
|
||||
public sealed class CallbackConfig
|
||||
{
|
||||
public string Url { get; set; } = "";
|
||||
public CallbackHttpMethod Method { get; set; } = CallbackHttpMethod.Post;
|
||||
public AuthMode AuthMode { get; set; } = AuthMode.None;
|
||||
public BearerOptions? Bearer { get; set; }
|
||||
public HmacOptions? Hmac { get; set; }
|
||||
public int TimeoutSeconds { get; set; } = 30;
|
||||
public int MaxAttempts { get; set; } = 5;
|
||||
public bool IncludeStdout { get; set; } = true;
|
||||
public bool IncludeStderr { get; set; } = true;
|
||||
public int MaxOutputBytes { get; set; } = 64 * 1024;
|
||||
public CallbackTrigger Trigger { get; set; } = CallbackTrigger.OnComplete;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace WebhookServer.Core.Models;
|
||||
|
||||
public sealed class DataPassingOptions
|
||||
{
|
||||
public bool StdinJson { get; set; }
|
||||
public bool EnvVars { get; set; }
|
||||
public bool ArgTemplate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whitespace-separated list of template tokens; each rendered token becomes one argv entry.
|
||||
/// Only used when <see cref="ArgTemplate"/> is true.
|
||||
/// </summary>
|
||||
public string? ArgTemplateString { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
namespace WebhookServer.Core.Models;
|
||||
|
||||
public sealed class EndpointConfig
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public string Slug { get; set; } = "";
|
||||
public string? Description { get; set; }
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
public List<string> AllowedClients { get; set; } = new();
|
||||
|
||||
public AuthMode AuthMode { get; set; } = AuthMode.None;
|
||||
public BearerOptions? Bearer { get; set; }
|
||||
public HmacOptions? Hmac { get; set; }
|
||||
|
||||
public ExecutorType ExecutorType { get; set; } = ExecutorType.WindowsPowerShell;
|
||||
|
||||
/// <summary>Path to a script file (.ps1, .bat, .cmd) when applicable.</summary>
|
||||
public string? ScriptPath { get; set; }
|
||||
|
||||
/// <summary>Inline command body when no script file is used (PowerShell -Command, cmd /c).</summary>
|
||||
public string? InlineCommand { get; set; }
|
||||
|
||||
/// <summary>Path to the executable when ExecutorType = Executable.</summary>
|
||||
public string? ExecutablePath { get; set; }
|
||||
|
||||
/// <summary>Static argv prefix for Executable mode; the rendered ArgTemplate appends after.</summary>
|
||||
public List<string> ExecutableArgs { get; set; } = new();
|
||||
|
||||
public string? WorkingDirectory { get; set; }
|
||||
|
||||
public DataPassingOptions DataPassing { get; set; } = new();
|
||||
|
||||
public ResponseMode ResponseMode { get; set; } = ResponseMode.Sync;
|
||||
|
||||
public int TimeoutSeconds { get; set; } = 60;
|
||||
|
||||
/// <summary>If true, a non-zero process exit produces 502 in sync mode (default true).</summary>
|
||||
public bool FailOnNonZeroExit { get; set; } = true;
|
||||
|
||||
/// <summary>If true, requests are processed one at a time per endpoint.</summary>
|
||||
public bool Serialize { get; set; }
|
||||
|
||||
public CallbackConfig? Callback { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
namespace WebhookServer.Core.Models;
|
||||
|
||||
public enum AuthMode
|
||||
{
|
||||
None = 0,
|
||||
Bearer = 1,
|
||||
Hmac = 2,
|
||||
}
|
||||
|
||||
public enum HmacAlgorithm
|
||||
{
|
||||
Sha1 = 1,
|
||||
Sha256 = 2,
|
||||
Sha512 = 3,
|
||||
}
|
||||
|
||||
public enum HmacEncoding
|
||||
{
|
||||
Hex = 0,
|
||||
Base64 = 1,
|
||||
}
|
||||
|
||||
public enum ExecutorType
|
||||
{
|
||||
WindowsPowerShell = 0,
|
||||
PwshCore = 1,
|
||||
Cmd = 2,
|
||||
Executable = 3,
|
||||
}
|
||||
|
||||
public enum ResponseMode
|
||||
{
|
||||
Sync = 0,
|
||||
Async = 1,
|
||||
}
|
||||
|
||||
public enum CallbackTrigger
|
||||
{
|
||||
OnComplete = 0,
|
||||
OnSuccess = 1,
|
||||
OnFailure = 2,
|
||||
}
|
||||
|
||||
public enum CallbackHttpMethod
|
||||
{
|
||||
Post = 0,
|
||||
Put = 1,
|
||||
}
|
||||
|
||||
public enum HttpsBindingKind
|
||||
{
|
||||
None = 0,
|
||||
PfxFile = 1,
|
||||
CertStoreThumbprint = 2,
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace WebhookServer.Core.Models;
|
||||
|
||||
public sealed class HmacOptions
|
||||
{
|
||||
public HmacAlgorithm Algorithm { get; set; } = HmacAlgorithm.Sha256;
|
||||
public string HeaderName { get; set; } = "X-Hub-Signature-256";
|
||||
public string Prefix { get; set; } = "sha256=";
|
||||
public HmacEncoding Encoding { get; set; } = HmacEncoding.Hex;
|
||||
public ProtectedString Secret { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace WebhookServer.Core.Models;
|
||||
|
||||
public sealed class HttpsBinding
|
||||
{
|
||||
public HttpsBindingKind Kind { get; set; } = HttpsBindingKind.None;
|
||||
public int Port { get; set; } = 8443;
|
||||
|
||||
/// <summary>Path to a .pfx file when Kind = PfxFile.</summary>
|
||||
public string? PfxPath { get; set; }
|
||||
public ProtectedString? PfxPassword { get; set; }
|
||||
|
||||
/// <summary>Cert thumbprint when Kind = CertStoreThumbprint.</summary>
|
||||
public string? Thumbprint { get; set; }
|
||||
public StoreLocation StoreLocation { get; set; } = StoreLocation.LocalMachine;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace WebhookServer.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A secret value. <see cref="Encrypted"/> is the persistent (DPAPI-protected) form;
|
||||
/// <see cref="Plaintext"/> is transient — the GUI sets it when submitting a new value
|
||||
/// over the named pipe, and the service sets it after decrypting on load. Disk JSON
|
||||
/// must never carry plaintext: <see cref="Storage.ConfigStore.SaveAsync"/> encrypts
|
||||
/// then clears <see cref="Plaintext"/> before writing.
|
||||
/// </summary>
|
||||
public sealed class ProtectedString
|
||||
{
|
||||
[JsonPropertyName("encrypted")]
|
||||
public string? Encrypted { get; set; }
|
||||
|
||||
[JsonPropertyName("plaintext")]
|
||||
public string? Plaintext { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool HasValue =>
|
||||
!string.IsNullOrEmpty(Encrypted) || !string.IsNullOrEmpty(Plaintext);
|
||||
|
||||
public static ProtectedString FromPlaintext(string value) =>
|
||||
new() { Plaintext = value };
|
||||
|
||||
public static ProtectedString FromEncrypted(string base64) =>
|
||||
new() { Encrypted = base64 };
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace WebhookServer.Core.Models;
|
||||
|
||||
public sealed class ServerConfig
|
||||
{
|
||||
public int HttpPort { get; set; } = 8080;
|
||||
public HttpsBinding? HttpsBinding { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// IPs/CIDRs allowed to set X-Forwarded-For. Empty = forwarded headers are ignored
|
||||
/// and the direct connection IP is always used.
|
||||
/// </summary>
|
||||
public List<string> TrustedProxies { get; set; } = new();
|
||||
|
||||
public int LogRetentionDays { get; set; } = 14;
|
||||
|
||||
public List<EndpointConfig> Endpoints { get; set; } = new();
|
||||
}
|
||||
Reference in New Issue
Block a user