From 959df5837f098feb76f58aa608b41eaa8707f209 Mon Sep 17 00:00:00 2001 From: Suz Hinton <54524269+suz-stripe@users.noreply.github.com> Date: Wed, 21 Jul 2021 16:02:47 -0700 Subject: [PATCH] dynamically deserialize request log event payload `request` field (#705) * dynamically deserialize event payload request data * rewind redundant rpc proto autogens * address feedback --- pkg/proxy/proxy.go | 48 +++++++++++++++++++++++++ pkg/proxy/stripeevent.go | 13 ++++--- pkg/rpcservice/events_resend.go | 10 ++++-- pkg/rpcservice/listen.go | 12 +++++-- pkg/rpcservice/listen_test.go | 63 +++++++++++++++++++++++++++++---- 5 files changed, 131 insertions(+), 15 deletions(-) diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index 99fdac7e..1becd1ae 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -10,6 +10,7 @@ import ( "net/http" "net/url" "os" + "reflect" "strconv" "strings" "time" @@ -321,6 +322,15 @@ func (p *Proxy) processWebhookEvent(msg websocket.IncomingMessage) { return } + req, err := ExtractRequestData(evt.RequestData) + + if err != nil { + p.cfg.Log.Debug("Received malformed event from Stripe, ignoring") + return + } + + evt.Request = req + p.cfg.Log.WithFields(log.Fields{ "prefix": "proxy.Proxy.processWebhookEvent", "webhook_id": webhookEvent.WebhookID, @@ -517,6 +527,44 @@ func Init(cfg *Config) (*Proxy, error) { return p, nil } +// ExtractRequestData takes an interface with request data from a Stripe event payload +// and properly parses it into a StripeRequest struct before returning it +func ExtractRequestData(data interface{}) (StripeRequest, error) { + var req StripeRequest + + reqDataValue := reflect.ValueOf(data) + // here we check the type of the RequestData as it could be either a string or a map + // this depends on which API version is in use when listening to events + // versions including and prior to 2017-05-25 present the request field as a string + switch reqDataValue.Kind() { + case reflect.String: + req = StripeRequest{ + ID: data.(string), + IdempotencyKey: "", + } + case reflect.Map: + reqDataMap := reqDataValue.Interface().(map[string]interface{}) + + var id = "" + if rawID, ok := reqDataMap["id"]; ok && rawID != nil { + id = rawID.(string) + } + + var idempotencyKey = "" + if rawKey, ok := reqDataMap["idempotency_key"]; ok && rawKey != nil { + idempotencyKey = rawKey.(string) + } + + req = StripeRequest{ + ID: id, + IdempotencyKey: idempotencyKey, + } + default: + return StripeRequest{}, errors.New("Received malformed event from Stripe") + } + return req, nil +} + // // Private types // diff --git a/pkg/proxy/stripeevent.go b/pkg/proxy/stripeevent.go index 29cc9928..ea2d87cd 100644 --- a/pkg/proxy/stripeevent.go +++ b/pkg/proxy/stripeevent.go @@ -3,6 +3,8 @@ package proxy import "fmt" // StripeEvent is a representation of a Stripe `event` object +// we define RequestData as an interface for backwards compatibility +// Request will hold the deserialized request data type StripeEvent struct { Account string `json:"account"` APIVersion string `json:"api_version"` @@ -10,15 +12,16 @@ type StripeEvent struct { Data map[string]interface{} `json:"data"` ID string `json:"id"` Livemode bool `json:"livemode"` - Request StripeRequestData `json:"request"` PendingWebhooks int `json:"pending_webhooks"` Type string `json:"type"` + RequestData interface{} `json:"request"` + Request StripeRequest } -// StripeRequestData is a representation of the Request field in a Stripe `event` object -type StripeRequestData struct { - ID string `json:"id"` - IdempotencyKey string `json:"idempotency_key"` +// StripeRequest is a representation of the Request field in a Stripe `event` object +type StripeRequest struct { + ID string + IdempotencyKey string } // IsConnect return true or false if *StripeEvent is connect or not. diff --git a/pkg/rpcservice/events_resend.go b/pkg/rpcservice/events_resend.go index 79cf4a93..7e9245ba 100644 --- a/pkg/rpcservice/events_resend.go +++ b/pkg/rpcservice/events_resend.go @@ -62,9 +62,15 @@ func (srv *RPCService) EventsResend(ctx context.Context, req *rpc.EventsResendRe return nil, err } + reqData, err := proxy.ExtractRequestData(evt.RequestData) + + if err != nil { + return nil, err + } + request := rpc.StripeEvent_Request{ - Id: evt.Request.ID, - IdempotencyKey: evt.Request.IdempotencyKey, + Id: reqData.ID, + IdempotencyKey: reqData.IdempotencyKey, } return &rpc.EventsResendResponse{ diff --git a/pkg/rpcservice/listen.go b/pkg/rpcservice/listen.go index dad44e0b..2e9be20a 100644 --- a/pkg/rpcservice/listen.go +++ b/pkg/rpcservice/listen.go @@ -183,10 +183,18 @@ func buildStripeEventResp(raw *proxy.StripeEvent) (*rpc.ListenResponse, error) { if err != nil { return nil, err } + + reqData, err := proxy.ExtractRequestData(raw.RequestData) + + if err != nil { + return nil, err + } + request := rpc.StripeEvent_Request{ - Id: raw.Request.ID, - IdempotencyKey: raw.Request.IdempotencyKey, + Id: reqData.ID, + IdempotencyKey: reqData.IdempotencyKey, } + return &rpc.ListenResponse{ Content: &rpc.ListenResponse_StripeEvent{ StripeEvent: &rpc.StripeEvent{ diff --git a/pkg/rpcservice/listen_test.go b/pkg/rpcservice/listen_test.go index 2054d894..ecf89f6e 100644 --- a/pkg/rpcservice/listen_test.go +++ b/pkg/rpcservice/listen_test.go @@ -110,9 +110,9 @@ func TestListenStreamsEvents(t *testing.T) { "id": "cs_test_12345", }, }, - Request: proxy.StripeRequestData{ - ID: "req_12345", - IdempotencyKey: "foo", + RequestData: map[string]interface{}{ + "id": "req_12345", + "idempotency_key": "foo", }, }, } @@ -430,9 +430,9 @@ func TestBuildStripeEventResponseSucceeds(t *testing.T) { "id": "cs_test_12345", }, }, - Request: proxy.StripeRequestData{ - ID: "req_12345", - IdempotencyKey: "foo", + RequestData: map[string]interface{}{ + "id": "req_12345", + "idempotency_key": "foo", }, } @@ -469,3 +469,54 @@ func TestBuildStripeEventResponseSucceeds(t *testing.T) { assert.Nil(t, err) assert.Equal(t, expected, actual) } + +func TestBuildLegacyStripeEventResponseSucceeds(t *testing.T) { + raw := &proxy.StripeEvent{ + Account: "acct_12345", + APIVersion: "2017-04-06", + Created: 12345, + ID: "evt_12345", + Livemode: false, + PendingWebhooks: 2, + Type: "checkout.session.completed", + Data: map[string]interface{}{ + "object": map[string]interface{}{ + "id": "cs_test_12345", + }, + }, + RequestData: "req_12345", + } + + expectedData, err := structpb.NewStruct(map[string]interface{}{ + "object": map[string]interface{}{ + "id": "cs_test_12345", + }, + }) + if err != nil { + t.Fatalf("Failed to create expected event data") + } + + expected := &rpc.ListenResponse{ + Content: &rpc.ListenResponse_StripeEvent{ + StripeEvent: &rpc.StripeEvent{ + Id: "evt_12345", + Account: "acct_12345", + ApiVersion: "2017-04-06", + Data: expectedData, + Type: "checkout.session.completed", + Created: 12345, + Livemode: false, + PendingWebhooks: 2, + Request: &rpc.StripeEvent_Request{ + Id: "req_12345", + IdempotencyKey: "", + }, + }, + }, + } + + actual, err := buildStripeEventResp(raw) + + assert.Nil(t, err) + assert.Equal(t, expected, actual) +}