mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-08 00:02:12 +00:00
117 lines
3.9 KiB
Go
117 lines
3.9 KiB
Go
package handler
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/warmbly/warmbly/internal/app/hubspot"
|
|
)
|
|
|
|
func hubspotSigned(t *testing.T, secret, signedURL, path, body string) *http.Request {
|
|
t.Helper()
|
|
ts := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
mac.Write([]byte(http.MethodPost + signedURL + body + ts))
|
|
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body))
|
|
req.Header.Set("X-HubSpot-Signature-v3", base64.StdEncoding.EncodeToString(mac.Sum(nil)))
|
|
req.Header.Set("X-HubSpot-Request-Timestamp", ts)
|
|
return req
|
|
}
|
|
|
|
func TestVerifyHubSpotUsesPublicURLOnly(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
t.Setenv("BACKEND_PUBLIC_URL", "https://api.example.com")
|
|
h := &Handler{HubSpot: hubspot.New(hubspot.Deps{ClientSecret: "shh"})}
|
|
path := "/api/v1/integrations/hubspot/webhooks"
|
|
body := `[]`
|
|
|
|
check := func(req *http.Request) bool {
|
|
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
c.Request = req
|
|
return h.verifyHubSpot(c, []byte(body))
|
|
}
|
|
if !check(hubspotSigned(t, "shh", "https://api.example.com"+path, path, body)) {
|
|
t.Fatal("a signature over the public URL was refused")
|
|
}
|
|
req := hubspotSigned(t, "shh", "https://attacker.example"+path, path, body)
|
|
req.Host = "attacker.example"
|
|
req.Header.Set("X-Forwarded-Proto", "https")
|
|
if check(req) {
|
|
t.Fatal("a signature over a client-chosen host was accepted")
|
|
}
|
|
v2 := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body))
|
|
v2.Header.Set("X-HubSpot-Signature", "deadbeef")
|
|
if check(v2) {
|
|
t.Fatal("an untimestamped signature was accepted")
|
|
}
|
|
}
|
|
|
|
func TestHubSpotServerRoutesRefuseCardFetches(t *testing.T) {
|
|
cases := map[string]bool{
|
|
"": false,
|
|
"portalId=1": true,
|
|
"appId=2": true,
|
|
"userId=3": true,
|
|
"userEmail=a%40b.com": true,
|
|
"PORTALID=1": true,
|
|
"other=1": false,
|
|
"portalId=1&userId=3&appId=2&x=1": true,
|
|
}
|
|
for raw, want := range cases {
|
|
q, err := url.ParseQuery(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := fromHubSpotFetch(q); got != want {
|
|
t.Errorf("fromHubSpotFetch(%q) = %v, want %v", raw, got, want)
|
|
}
|
|
}
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
h := &Handler{HubSpot: hubspot.New(hubspot.Deps{ClientSecret: "shh"})}
|
|
for _, route := range []func(*gin.Context){h.HubSpotWebhook, h.HubSpotActionEnroll, h.HubSpotActionCampaigns} {
|
|
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
c.Request = httptest.NewRequest(http.MethodPost, "/x?portalId=1&userId=3&userEmail=a%40b.com&appId=2", strings.NewReader(`{}`))
|
|
route(c)
|
|
if got := c.Writer.Status(); got != http.StatusForbidden {
|
|
t.Errorf("a card fetch to a server route answered %d, want 403", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHubSpotFetchUserNeedsOneOfEach(t *testing.T) {
|
|
cases := map[string]bool{
|
|
"portalId=1&userId=2&userEmail=a%40b.com": true,
|
|
"portalId=1&userId=2": false,
|
|
"portalId=1&userEmail=a%40b.com": false,
|
|
"userId=2&userEmail=a%40b.com": false,
|
|
"portalId=1&userId=2&userEmail=a%40b.com&userEmail=c%40d.com": false,
|
|
"portalId=1&userId=2&userId=9&userEmail=a%40b.com": false,
|
|
"portalId=1&portalId=9&userId=2&userEmail=a%40b.com": false,
|
|
"portalId=1&userId=2&userEmail=+": false,
|
|
}
|
|
for raw, want := range cases {
|
|
q, err := url.ParseQuery(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
u, ok := hubspotFetchUser(q)
|
|
if ok != want {
|
|
t.Errorf("hubspotFetchUser(%q) ok = %v, want %v", raw, ok, want)
|
|
}
|
|
if ok && (u.ID != "2" || u.Email != "a@b.com") {
|
|
t.Errorf("hubspotFetchUser(%q) = %+v", raw, u)
|
|
}
|
|
}
|
|
}
|