Skip to content

Commit

Permalink
Move endpoint response output out of Proxy (#674)
Browse files Browse the repository at this point in the history
  • Loading branch information
vcheung-stripe committed May 20, 2021
1 parent 594a477 commit 18da08e
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 75 deletions.
82 changes: 63 additions & 19 deletions pkg/cmd/listen.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,37 @@ func createVisitor(logger *log.Logger, format string, printJSON bool) *websocket
return &websocket.Visitor{
VisitError: func(ee websocket.ErrorElement) error {
ansi.StopSpinner(s, "", logger.Out)
logger.Fatal(ee.Error)
return ee.Error
switch ee.Error.(type) {
case proxy.FailedToPostError:
color := ansi.Color(os.Stdout)
localTime := time.Now().Format(timeLayout)

errStr := fmt.Sprintf("%s [%s] Failed to POST: %v\n",
color.Faint(localTime),
color.Red("ERROR"),
ee.Error,
)
fmt.Println(errStr)

// Don't exit program
return nil
case proxy.FailedToReadResponseError:
color := ansi.Color(os.Stdout)
localTime := time.Now().Format(timeLayout)

errStr := fmt.Sprintf("%s [%s] Failed to read response from endpoint, error = %v\n",
color.Faint(localTime),
color.Red("ERROR"),
ee.Error,
)
log.Errorf(errStr)

// Don't exit program
return nil
default:
logger.Fatal(ee.Error)
return ee.Error
}
},
VisitStatus: func(se websocket.StateElement) error {
switch se.State {
Expand All @@ -219,31 +248,46 @@ func createVisitor(logger *log.Logger, format string, printJSON bool) *websocket
return nil
},
VisitData: func(de websocket.DataElement) error {
stripeEvent, ok := de.Data.(proxy.StripeEvent)
if !ok {
return fmt.Errorf("VisitData received unexpected type for DataElement, got %T expected %T", de, proxy.StripeEvent{})
}

if strings.ToUpper(format) == outputFormatJSON || printJSON {
fmt.Println(de.Marshaled)
} else {
maybeConnect := ""
if stripeEvent.IsConnect() {
maybeConnect = "connect "
switch data := de.Data.(type) {
case proxy.StripeEvent:
if strings.ToUpper(format) == outputFormatJSON || printJSON {
fmt.Println(de.Marshaled)
} else {
maybeConnect := ""
if data.IsConnect() {
maybeConnect = "connect "
}

localTime := time.Now().Format(timeLayout)

color := ansi.Color(os.Stdout)
outputStr := fmt.Sprintf("%s --> %s%s [%s]",
color.Faint(localTime),
maybeConnect,
ansi.Linkify(ansi.Bold(data.Type), data.URLForEventType(), logger.Out),
ansi.Linkify(data.ID, data.URLForEventID(), logger.Out),
)
fmt.Println(outputStr)
}

return nil
case proxy.EndpointResponse:
event := data.Event
resp := data.Resp
localTime := time.Now().Format(timeLayout)

color := ansi.Color(os.Stdout)
outputStr := fmt.Sprintf("%s --> %s%s [%s]",
outputStr := fmt.Sprintf("%s <-- [%d] %s %s [%s]",
color.Faint(localTime),
maybeConnect,
ansi.Linkify(ansi.Bold(stripeEvent.Type), stripeEvent.URLForEventType(), logger.Out),
ansi.Linkify(stripeEvent.ID, stripeEvent.URLForEventID(), logger.Out),
ansi.ColorizeStatus(resp.StatusCode),
resp.Request.Method,
resp.Request.URL,
ansi.Linkify(event.ID, event.URLForEventID(), logger.Out),
)
fmt.Println(outputStr)
return nil
default:
return fmt.Errorf("VisitData received unexpected type for DataElement, got %T", de)
}
return nil
},
}
}
29 changes: 16 additions & 13 deletions pkg/proxy/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@ package proxy

import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"os"
"regexp"
"strings"
"time"

log "github.com/sirupsen/logrus"

"github.com/stripe/stripe-cli/pkg/ansi"
"github.com/stripe/stripe-cli/pkg/websocket"
)

//
Expand All @@ -26,6 +24,9 @@ type EndpointConfig struct {
Log *log.Logger

ResponseHandler EndpointResponseHandler

// OutCh is the channel to send data and statuses to for processing in other packages
OutCh chan websocket.IElement
}

// EndpointResponseHandler handles a response from the endpoint.
Expand All @@ -44,6 +45,15 @@ func (f EndpointResponseHandlerFunc) ProcessResponse(evtCtx eventContext, forwar
f(evtCtx, forwardURL, resp)
}

// FailedToPostError describes a failure to send a POST request to an endpoint
type FailedToPostError struct {
err error
}

func (f FailedToPostError) Error() string {
return f.err.Error()
}

// EndpointClient is the client used to POST webhook requests to the local endpoint.
type EndpointClient struct {
// URL the client sends POST requests to
Expand Down Expand Up @@ -100,16 +110,9 @@ func (c *EndpointClient) Post(evtCtx eventContext, body string, headers map[stri

resp, err := c.cfg.HTTPClient.Do(req)
if err != nil {
color := ansi.Color(os.Stdout)
localTime := time.Now().Format(timeLayout)

errStr := fmt.Sprintf("%s [%s] Failed to POST: %v\n",
color.Faint(localTime),
color.Red("ERROR"),
err,
)
fmt.Println(errStr)

c.cfg.OutCh <- websocket.ErrorElement{
Error: FailedToPostError{err: err},
}
return err
}

Expand Down
77 changes: 48 additions & 29 deletions pkg/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ import (
"github.com/stripe/stripe-cli/pkg/websocket"
)

const timeLayout = "2006-01-02 15:04:05"

//
// Public types
//
Expand All @@ -45,6 +43,22 @@ type EndpointRoute struct {
EventTypes []string
}

// EndpointResponse describes the response to a Stripe event from an endpoint
type EndpointResponse struct {
Event *StripeEvent
Resp *http.Response
RespBody string
}

// FailedToReadResponseError describes a failure to read the response from an endpoint
type FailedToReadResponseError struct {
err error
}

func (f FailedToReadResponseError) Error() string {
return f.err.Error()
}

// Config provides the configuration of a Proxy
type Config struct {
// DeviceName is the name of the device sent to Stripe to help identify the device
Expand Down Expand Up @@ -346,32 +360,24 @@ func (p *Proxy) processWebhookEvent(msg websocket.IncomingMessage) {
}

func (p *Proxy) processEndpointResponse(evtCtx eventContext, forwardURL string, resp *http.Response) {
localTime := time.Now().Format(timeLayout)

color := ansi.Color(os.Stdout)
outputStr := fmt.Sprintf("%s <-- [%d] %s %s [%s]",
color.Faint(localTime),
ansi.ColorizeStatus(resp.StatusCode),
resp.Request.Method,
resp.Request.URL,
ansi.Linkify(evtCtx.event.ID, evtCtx.event.URLForEventID(), p.cfg.Log.Out),
)
fmt.Println(outputStr)

buf, err := ioutil.ReadAll(resp.Body)
if err != nil {
errStr := fmt.Sprintf("%s [%s] Failed to read response from endpoint, error = %v\n",
color.Faint(localTime),
color.Red("ERROR"),
err,
)
log.Errorf(errStr)

p.cfg.OutCh <- websocket.ErrorElement{
Error: FailedToReadResponseError{err: err},
}
return
}

body := truncate(string(buf), maxBodySize, true)

p.cfg.OutCh <- websocket.DataElement{
Data: EndpointResponse{
Event: evtCtx.event,
Resp: resp,
RespBody: body,
},
}

idx := 0
headers := make(map[string]string)

Expand Down Expand Up @@ -426,7 +432,11 @@ func Init(cfg *Config) (*Proxy, error) {
if len(endpoints.Data) == 0 {
return nil, errors.New("You have not defined any webhook endpoints on your account. Go to the Stripe Dashboard to add some: https://dashboard.stripe.com/test/webhooks")
}
endpointRoutes = buildEndpointRoutes(endpoints, parseURL(cfg.ForwardURL), parseURL(cfg.ForwardConnectURL), cfg.ForwardHeaders, cfg.ForwardConnectHeaders)
var err error
endpointRoutes, err = buildEndpointRoutes(endpoints, parseURL(cfg.ForwardURL), parseURL(cfg.ForwardConnectURL), cfg.ForwardHeaders, cfg.ForwardConnectHeaders)
if err != nil {
return nil, err
}
} else {
// build from --forward-to urls
if len(cfg.ForwardConnectURL) == 0 {
Expand Down Expand Up @@ -483,6 +493,7 @@ func Init(cfg *Config) (*Proxy, error) {
},
Log: p.cfg.Log,
ResponseHandler: EndpointResponseHandlerFunc(p.processEndpointResponse),
OutCh: p.cfg.OutCh,
},
))
}
Expand Down Expand Up @@ -582,7 +593,7 @@ func getEndpointsFromAPI(secretKey, apiBaseURL string) requests.WebhookEndpointL
return requests.WebhookEndpointsList(apiBaseURL, "2019-03-14", secretKey, &config.Profile{})
}

func buildEndpointRoutes(endpoints requests.WebhookEndpointList, forwardURL, forwardConnectURL string, forwardHeaders []string, forwardConnectHeaders []string) []EndpointRoute {
func buildEndpointRoutes(endpoints requests.WebhookEndpointList, forwardURL, forwardConnectURL string, forwardHeaders []string, forwardConnectHeaders []string) ([]EndpointRoute, error) {
endpointRoutes := make([]EndpointRoute, 0)

for _, endpoint := range endpoints.Data {
Expand All @@ -592,15 +603,23 @@ func buildEndpointRoutes(endpoints requests.WebhookEndpointList, forwardURL, for
// Since webhooks in the dashboard may have a more generic url, only extract
// the path. We'll use this with `localhost` or with the `--forward-to` flag
if endpoint.Application == "" {
url, err := buildForwardURL(forwardURL, u)
if err != nil {
return nil, err
}
endpointRoutes = append(endpointRoutes, EndpointRoute{
URL: buildForwardURL(forwardURL, u),
URL: url,
ForwardHeaders: forwardHeaders,
Connect: false,
EventTypes: endpoint.EnabledEvents,
})
} else {
url, err := buildForwardURL(forwardConnectURL, u)
if err != nil {
return nil, err
}
endpointRoutes = append(endpointRoutes, EndpointRoute{
URL: buildForwardURL(forwardConnectURL, u),
URL: url,
ForwardHeaders: forwardConnectHeaders,
Connect: true,
EventTypes: endpoint.EnabledEvents,
Expand All @@ -609,13 +628,13 @@ func buildEndpointRoutes(endpoints requests.WebhookEndpointList, forwardURL, for
}
}

return endpointRoutes
return endpointRoutes, nil
}

func buildForwardURL(forwardURL string, destination *url.URL) string {
func buildForwardURL(forwardURL string, destination *url.URL) (string, error) {
f, err := url.Parse(forwardURL)
if err != nil {
log.Fatalf("Provided forward url cannot be parsed: %s", forwardURL)
return "", fmt.Errorf("Provided forward url cannot be parsed: %s", forwardURL)
}

return fmt.Sprintf(
Expand All @@ -624,7 +643,7 @@ func buildForwardURL(forwardURL string, destination *url.URL) string {
f.Host,
strings.TrimSuffix(f.Path, "/"), // avoids having a double "//"
destination.Path,
)
), nil
}

func getAPIVersionString(str *string) string {
Expand Down
48 changes: 34 additions & 14 deletions pkg/proxy/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ func TestBuildEndpointRoutes(t *testing.T) {
Data: []requests.WebhookEndpoint{endpointNormal, endpointConnect},
}

output := buildEndpointRoutes(endpointList, localURL, localURL, []string{"Host: hostname"}, []string{"Host: connecthostname"})
output, err := buildEndpointRoutes(endpointList, localURL, localURL, []string{"Host: hostname"}, []string{"Host: connecthostname"})
require.NoError(t, err)
require.Equal(t, 2, len(output))
require.Equal(t, "http://localhost/hooks", output[0].URL)
require.Equal(t, []string{"Host: hostname"}, output[0].ForwardHeaders)
Expand All @@ -79,23 +80,42 @@ func TestBuildForwardURL(t *testing.T) {
f, err := url.Parse("http://example.com/foo/bar.php")
require.NoError(t, err)

require.Equal(t, "http://localhost/foo/bar.php", buildForwardURL("http://localhost/", f))
require.Equal(t, "http://localhost/foo/bar.php", buildForwardURL("http://localhost", f))
require.Equal(t, "https://localhost/foo/bar.php", buildForwardURL("https://localhost/", f))
require.Equal(t, "http://localhost:8000/foo/bar.php", buildForwardURL("http://localhost:8000", f))
require.Equal(t, "http://localhost:8000/foo/bar.php", buildForwardURL("http://localhost:8000/", f))
require.Equal(t, "http://localhost:8000/forward/sub/path/foo/bar.php", buildForwardURL("http://localhost:8000/forward/sub/path/", f))
require.Equal(t, "http://localhost:8000/forward/sub/path/foo/bar.php", buildForwardURL("http://localhost:8000/forward/sub/path", f))
// pairs of [expected, input]
expectedInputPairs := [][]string{
{"http://localhost/foo/bar.php", "http://localhost"},
{"https://localhost/foo/bar.php", "https://localhost/"},
{"http://localhost:8000/foo/bar.php", "http://localhost:8000"},
{"http://localhost:8000/foo/bar.php", "http://localhost:8000/"},
{"http://localhost:8000/forward/sub/path/foo/bar.php", "http://localhost:8000/forward/sub/path/"},
{"http://localhost:8000/forward/sub/path/foo/bar.php", "http://localhost:8000/forward/sub/path"},
}
for _, pair := range expectedInputPairs {
expected := pair[0]
input := pair[1]
forwardURL, err := buildForwardURL(input, f)
require.NoError(t, err)
require.Equal(t, expected, forwardURL)
}

f, err = url.Parse("http://example.com/bar/")
require.NoError(t, err)

require.Equal(t, "http://localhost/bar/", buildForwardURL("http://localhost/", f))
require.Equal(t, "http://localhost/bar/", buildForwardURL("http://localhost", f))
require.Equal(t, "https://localhost/bar/", buildForwardURL("https://localhost/", f))
require.Equal(t, "https://localhost/bar/", buildForwardURL("https://localhost", f))
require.Equal(t, "http://localhost:8000/bar/", buildForwardURL("http://localhost:8000", f))
require.Equal(t, "http://localhost:8000/bar/", buildForwardURL("http://localhost:8000/", f))
// pairs of [expected, input]
expectedInputPairs = [][]string{
{"http://localhost/bar/", "http://localhost/"},
{"http://localhost/bar/", "http://localhost"},
{"https://localhost/bar/", "https://localhost/"},
{"https://localhost/bar/", "https://localhost"},
{"http://localhost:8000/bar/", "http://localhost:8000"},
{"http://localhost:8000/bar/", "http://localhost:8000/"},
}
for _, pair := range expectedInputPairs {
expected := pair[0]
input := pair[1]
forwardURL, err := buildForwardURL(input, f)
require.NoError(t, err)
require.Equal(t, expected, forwardURL)
}
}

func TestParseUrl(t *testing.T) {
Expand Down

0 comments on commit 18da08e

Please sign in to comment.