forked from wrenn/wrenn
Implement a channels system for notifying teams via external providers
(Discord, Slack, Teams, Google Chat, Telegram, Matrix, webhook) when
lifecycle events occur (capsule/template/host state changes).
- Channel CRUD API under /v1/channels (JWT-only auth)
- Test endpoint to verify config before saving (POST /v1/channels/test)
- Secret rotation endpoint (PUT /v1/channels/{id}/config)
- AES-256-GCM encryption for provider secrets (WRENN_ENCRYPTION_KEY)
- Redis stream event publishing from audit logger
- Background dispatcher with consumer group and retry (10s, 30s)
- Webhook delivery with HMAC-SHA256 signing (X-WRENN-SIGNATURE)
- shoutrrr integration for chat providers
- Secrets never exposed in API responses
63 lines
1.6 KiB
Go
63 lines
1.6 KiB
Go
package channels
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// WebhookDelivery delivers events to webhook URLs with HMAC signing.
|
|
type WebhookDelivery struct {
|
|
client *http.Client
|
|
}
|
|
|
|
// NewWebhookDelivery constructs a webhook delivery client.
|
|
func NewWebhookDelivery() *WebhookDelivery {
|
|
return &WebhookDelivery{
|
|
client: &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
CheckRedirect: func(*http.Request, []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Deliver signs and POSTs the event payload to the configured URL.
|
|
func (d *WebhookDelivery) Deliver(ctx context.Context, targetURL, secret string, payload []byte) error {
|
|
timestamp := time.Now().UTC().Format(time.RFC3339)
|
|
deliveryID := uuid.New().String()
|
|
|
|
// Compute HMAC-SHA256: sign over "timestamp.body".
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
mac.Write([]byte(timestamp + "." + string(payload)))
|
|
signature := "sha256=" + hex.EncodeToString(mac.Sum(nil))
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, targetURL, strings.NewReader(string(payload)))
|
|
if err != nil {
|
|
return fmt.Errorf("create request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-WRENN-SIGNATURE", signature)
|
|
req.Header.Set("X-Wrenn-Delivery", deliveryID)
|
|
req.Header.Set("X-Wrenn-Timestamp", timestamp)
|
|
|
|
resp, err := d.client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("http post: %w", err)
|
|
}
|
|
resp.Body.Close()
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return fmt.Errorf("webhook returned %d", resp.StatusCode)
|
|
}
|
|
return nil
|
|
}
|