-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
156 lines (137 loc) · 5.75 KB
/
Program.cs
File metadata and controls
156 lines (137 loc) · 5.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// ---------------------------------------------------------------
// WebhookEngine Sample — Receiver (Webhook Consumer)
// ---------------------------------------------------------------
// This minimal API receives webhooks from WebhookEngine,
// verifies the HMAC-SHA256 signature, and logs the payload.
//
// Usage:
// dotnet run
// (Listens on http://localhost:5200 by default)
//
// Environment variables:
// WEBHOOK_SECRET — The signing secret for your application
// (found in the dashboard or application settings)
//
// Signature verification follows the Standard Webhooks spec:
// Header: webhook-signature = "v1,<base64>"
// Signed content: "{webhook-id}.{webhook-timestamp}.{body}"
// ---------------------------------------------------------------
using System.Security.Cryptography;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://localhost:5200");
var app = builder.Build();
app.MapPost("/webhook", async (HttpContext context) =>
{
// Read headers
var webhookId = context.Request.Headers["webhook-id"].FirstOrDefault();
var webhookTimestamp = context.Request.Headers["webhook-timestamp"].FirstOrDefault();
var webhookSignature = context.Request.Headers["webhook-signature"].FirstOrDefault();
// Read body
context.Request.EnableBuffering();
using var reader = new StreamReader(context.Request.Body, Encoding.UTF8, leaveOpen: true);
var body = await reader.ReadToEndAsync();
// Log the received webhook
var separator = new string('=', 60);
Console.WriteLine();
Console.WriteLine(separator);
Console.WriteLine($" WEBHOOK RECEIVED [{DateTime.UtcNow:HH:mm:ss.fff}]");
Console.WriteLine(separator);
Console.WriteLine($" webhook-id: {webhookId ?? "(missing)"}");
Console.WriteLine($" webhook-timestamp: {webhookTimestamp ?? "(missing)"}");
Console.WriteLine($" webhook-signature: {webhookSignature ?? "(missing)"}");
Console.WriteLine($" content-type: {context.Request.ContentType}");
Console.WriteLine($" body length: {body.Length} bytes");
Console.WriteLine();
// Pretty-print payload (best effort)
try
{
var jsonDoc = System.Text.Json.JsonDocument.Parse(body);
var prettyBody = System.Text.Json.JsonSerializer.Serialize(
jsonDoc, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(" Payload:");
foreach (var line in prettyBody.Split('\n'))
Console.WriteLine($" {line}");
}
catch
{
Console.WriteLine($" Payload (raw): {body}");
}
// Verify signature
var secret = Environment.GetEnvironmentVariable("WEBHOOK_SECRET");
if (string.IsNullOrWhiteSpace(secret))
{
Console.WriteLine();
Console.WriteLine(" [WARN] WEBHOOK_SECRET not set — skipping signature verification.");
Console.WriteLine(" Set it to enable HMAC-SHA256 verification.");
Console.WriteLine(separator);
return Results.Ok(new { status = "received", verified = false });
}
if (string.IsNullOrEmpty(webhookId) || string.IsNullOrEmpty(webhookTimestamp) || string.IsNullOrEmpty(webhookSignature))
{
Console.WriteLine(" [FAIL] Missing required webhook headers.");
Console.WriteLine(separator);
return Results.BadRequest(new { error = "Missing webhook headers" });
}
// Check timestamp tolerance (5 minutes)
if (long.TryParse(webhookTimestamp, out var ts))
{
var messageTime = DateTimeOffset.FromUnixTimeSeconds(ts);
var drift = Math.Abs((DateTimeOffset.UtcNow - messageTime).TotalMinutes);
if (drift > 5)
{
Console.WriteLine($" [FAIL] Timestamp too old/new (drift: {drift:F1} min).");
Console.WriteLine(separator);
return Results.BadRequest(new { error = "Timestamp out of tolerance" });
}
}
// Compute expected signature
var signedContent = $"{webhookId}.{webhookTimestamp}.{body}";
var secretBytes = ResolveSecretBytes(secret);
using var hmac = new HMACSHA256(secretBytes);
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signedContent));
var expectedSignature = $"v1,{Convert.ToBase64String(hash)}";
// Compare — webhook-signature can contain multiple signatures (v1,sig1 v1,sig2)
var signatures = webhookSignature.Split(' ');
var verified = signatures.Any(s => string.Equals(s.Trim(), expectedSignature, StringComparison.Ordinal));
Console.WriteLine();
if (verified)
{
Console.WriteLine(" [OK] Signature verified successfully.");
}
else
{
Console.WriteLine(" [FAIL] Signature mismatch!");
Console.WriteLine($" Expected: {expectedSignature}");
Console.WriteLine($" Received: {webhookSignature}");
}
Console.WriteLine(separator);
return verified
? Results.Ok(new { status = "received", verified = true })
: Results.Unauthorized();
});
app.MapGet("/", () => Results.Ok(new
{
service = "WebhookEngine Sample Receiver",
status = "running",
endpoint = "POST /webhook"
}));
Console.WriteLine("WebhookEngine Sample Receiver");
Console.WriteLine("Listening on http://localhost:5200");
Console.WriteLine("Webhook endpoint: POST http://localhost:5200/webhook");
Console.WriteLine("Waiting for webhooks...\n");
app.Run();
// --- Helper: Resolve secret bytes (matches HmacSigningService logic) ---
static byte[] ResolveSecretBytes(string secret)
{
if (secret.StartsWith("whsec_", StringComparison.OrdinalIgnoreCase))
return Encoding.UTF8.GetBytes(secret);
try
{
return Convert.FromBase64String(secret);
}
catch (FormatException)
{
return Encoding.UTF8.GetBytes(secret);
}
}