Skip to content

Commit

Permalink
Merge pull request #77 from blockfrost/chore/webhook-impr
Browse files Browse the repository at this point in the history
chore: add common webhook fields to WebhookEvent
  • Loading branch information
slowbackspace committed Jan 17, 2024
2 parents ae3b268 + e21e666 commit fe321c8
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 41 deletions.
91 changes: 91 additions & 0 deletions example/webhook/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// This example shows how to implement a webhook endpoint for receiving Blockfrost Webhook requests
// https://blockfrost.dev/start-building/webhooks/
// Run using: go run example/webhook/main.go
package main

import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"

"github.com/blockfrost/blockfrost-go"
)

const SECRET_AUTH_TOKEN string = "dc8f4b23-6c44-405f-8940-5d451222458e"

func main() {
http.HandleFunc("/webhook", func(w http.ResponseWriter, req *http.Request) {
const MaxBodyBytes = int64(524288)
req.Body = http.MaxBytesReader(w, req.Body, MaxBodyBytes)
payload, err := io.ReadAll(req.Body)

fmt.Printf("Received webhook request.\n")

if err != nil {
fmt.Fprintf(os.Stderr, "Error reading request body: %v\n", err)
w.WriteHeader(http.StatusServiceUnavailable)
return
}

event, err := blockfrost.VerifyWebhookSignature(payload, req.Header.Get("Blockfrost-Signature"), SECRET_AUTH_TOKEN)

if err != nil {
fmt.Fprintf(os.Stderr, "Error verifying webhook signature: %v\n", err)
w.WriteHeader(http.StatusBadRequest)
return
}

// Unmarshal the event data into an appropriate struct depending on its Type
switch event.Type {
case "block":
var blockEvent blockfrost.WebhookEventBlock
err := json.Unmarshal(payload, &blockEvent)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err)
w.WriteHeader(http.StatusBadRequest)
return
}
fmt.Printf("Received block event: %v\n", blockEvent)
case "transaction":
var transactionEvent blockfrost.WebhookEventTransaction
err := json.Unmarshal(payload, &transactionEvent)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err)
w.WriteHeader(http.StatusBadRequest)
return
}
fmt.Printf("Received tx event: %v\n", transactionEvent)
case "epoch":
var epochEvent blockfrost.WebhookEventEpoch
err := json.Unmarshal(payload, &epochEvent)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err)
w.WriteHeader(http.StatusBadRequest)
return
}
fmt.Printf("Received epoch event: %v\n", epochEvent)
case "delegation":
var delegationEvent blockfrost.WebhookEventDelegation
err := json.Unmarshal(payload, &delegationEvent)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err)
w.WriteHeader(http.StatusBadRequest)
return
}
fmt.Printf("Received delegation event: %v\n", delegationEvent)
default:
fmt.Fprintf(os.Stderr, "Unhandled webhook type: %s\n", event.Type)
}

w.WriteHeader(http.StatusOK)
})

fmt.Println("Server is starting on port 8080...")

err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Fprintf(os.Stderr, "Server failed to start: %v\n", err)
}
}
51 changes: 15 additions & 36 deletions webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,33 +49,32 @@ type WebhookEventCommon struct {
WebhookID string `json:"webhook_id"`
Created int `json:"created"`
APIVersion int `json:"api_version,omitempty"` // omitempty because test fixtures do not include it
Type string `json:"type"` // block, transaction, delegation, epoch
}

type WebhookEventBlock struct {
WebhookEventCommon
Type string `json:"type"` // "block"
Payload Block `json:"payload"`
Payload Block `json:"payload"`
}

type WebhookEventTransaction struct {
WebhookEventCommon
Type string `json:"type"` // "transaction"
Payload []TransactionPayload `json:"payload"`
}

type WebhookEventEpoch struct {
WebhookEventCommon
Type string `json:"type"` // "epoch"
Payload EpochPayload `json:"payload"`
}

type WebhookEventDelegation struct {
WebhookEventCommon
Type string `json:"type"` // "delegation"
Payload []StakeDelegationPayload `json:"payload"`
}

type WebhookEvent interface{}
type WebhookEvent struct {
WebhookEventCommon
}

const (
// Signatures older than this will be rejected by ConstructEvent
Expand Down Expand Up @@ -152,67 +151,47 @@ func parseSignatureHeader(header string) (*signedHeader, error) {
return sh, nil
}

func VerifyWebhookSignature(payload []byte, header string, secret string) (WebhookEvent, error) {
func VerifyWebhookSignature(payload []byte, header string, secret string) (*WebhookEvent, error) {
return VerifyWebhookSignatureWithTolerance(payload, header, secret, DefaultTolerance)
}

func VerifyWebhookSignatureWithTolerance(payload []byte, header string, secret string, tolerance time.Duration) (WebhookEvent, error) {
func VerifyWebhookSignatureWithTolerance(payload []byte, header string, secret string, tolerance time.Duration) (*WebhookEvent, error) {
return verifyWebhookSignature(payload, header, secret, tolerance, true)
}

func VerifyWebhookSignatureIgnoringTolerance(payload []byte, header string, secret string) (WebhookEvent, error) {
func VerifyWebhookSignatureIgnoringTolerance(payload []byte, header string, secret string) (*WebhookEvent, error) {
return verifyWebhookSignature(payload, header, secret, 0*time.Second, false)
}

func verifyWebhookSignature(payload []byte, sigHeader string, secret string, tolerance time.Duration, enforceTolerance bool) (WebhookEvent, error) {
func verifyWebhookSignature(payload []byte, sigHeader string, secret string, tolerance time.Duration, enforceTolerance bool) (*WebhookEvent, error) {
// First unmarshal into a generic map to inspect the type
var genericEvent map[string]interface{}
if err := json.Unmarshal(payload, &genericEvent); err != nil {
return nil, fmt.Errorf("failed to parse webhook body json: %s", err)
}

// Determine the specific event type
eventType, ok := genericEvent["type"].(string)
if !ok {
return nil, errors.New("event type not found")
return nil, fmt.Errorf("Failed to parse webhook body json: %s", err)
}

var event WebhookEvent

// Unmarshal into the specific event type based on the eventType
switch eventType {
case string(WebhookEventTypeBlock):
event = new(WebhookEventBlock)
case string(WebhookEventTypeTransaction):
event = new(WebhookEventTransaction)
case string(WebhookEventTypeEpoch):
event = new(WebhookEventEpoch)
case string(WebhookEventTypeDelegation):
event = new(WebhookEventDelegation)
default:
return nil, fmt.Errorf("unknown event type: %s", eventType)
}

if err := json.Unmarshal(payload, &event); err != nil {
return nil, fmt.Errorf("failed to parse specific webhook event json: %s", err)
return nil, fmt.Errorf("Failed to parse specific webhook event json: %s", err)
}

header, err := parseSignatureHeader(sigHeader)
if err != nil {
return event, err
return &event, err
}

expectedSignature := computeSignature(header.timestamp, payload, secret)
expiredTimestamp := time.Since(header.timestamp) > tolerance
if enforceTolerance && expiredTimestamp {
return event, ErrTooOld
return &event, ErrTooOld
}

for _, sig := range header.signatures {
if hmac.Equal(expectedSignature, sig) {
return event, nil
return &event, nil
}
}

return event, ErrNoValidSignature
return &event, ErrNoValidSignature
}
18 changes: 13 additions & 5 deletions webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,21 @@ func TestVerifyWebhookSignature(t *testing.T) {
"t=1650013856,v1=abc,t=1650013856,v1=f4c3bb2a8b0c8e21fa7d5fdada2ee87c9c6f6b0b159cc22e483146917e195c3e",
"59a1eb46-96f4-4f0b-8a03-b4d26e70593a")

_, ok := event.(*blockfrost.WebhookEventBlock)
if !ok {
jsonData, _ := json.Marshal(event)
t.Fatalf("Invalid webhook type %s", jsonData)
// Unmarshal the event data into an appropriate struct depending on its Type
var blockEvent blockfrost.WebhookEventBlock
switch event.Type {
case "block":
err := json.Unmarshal([]byte(validPayload), &blockEvent)

if err != nil {
t.Fatalf("Error parsing webhook JSON: %v\n", err)
return
}
default:
t.Fatalf("Invalid webhook type: %s\n", event.Type)
}

jsonData, err := json.Marshal(event)
jsonData, err := json.Marshal(blockEvent)
if err != nil {
t.Fatalf("Error marshaling to JSON: %s", err)
}
Expand Down

0 comments on commit fe321c8

Please sign in to comment.