mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
261 lines
6.4 KiB
Go
261 lines
6.4 KiB
Go
package hooks
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/photoprism/photoprism/pkg/media/http/header"
|
|
)
|
|
|
|
var defaultMsgID = "msg_p5jXN8AQM9LWM0D4loKWxJek"
|
|
var defaultPayload = []byte(`{"test": 2432232314}`)
|
|
var defaultSecret = "MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
|
|
|
|
type testPayload struct {
|
|
id string
|
|
timestamp time.Time
|
|
header http.Header
|
|
secret string
|
|
payload []byte
|
|
signature string
|
|
}
|
|
|
|
func newTestPayload(timestamp time.Time) *testPayload {
|
|
tp := &testPayload{}
|
|
tp.id = defaultMsgID
|
|
tp.timestamp = timestamp
|
|
|
|
tp.payload = defaultPayload
|
|
tp.secret = defaultSecret
|
|
|
|
wh, _ := NewSecret(tp.secret)
|
|
tp.signature, _ = wh.Sign(tp.id, tp.timestamp, tp.payload)
|
|
|
|
tp.header = http.Header{}
|
|
tp.header.Set(header.WebhookID, tp.id)
|
|
tp.header.Set(header.WebhookSignature, tp.signature)
|
|
tp.header.Set(header.WebhookTimestamp, fmt.Sprint(tp.timestamp.Unix()))
|
|
|
|
return tp
|
|
}
|
|
|
|
func TestWebhook(t *testing.T) {
|
|
|
|
testCases := []struct {
|
|
name string
|
|
testPayload *testPayload
|
|
modifyPayload func(*testPayload)
|
|
noEnforceTimestamp bool
|
|
expectedErr bool
|
|
}{
|
|
{
|
|
name: "valid signature is valid",
|
|
testPayload: newTestPayload(time.Now()),
|
|
expectedErr: false,
|
|
},
|
|
{
|
|
name: "missing id returns error",
|
|
testPayload: newTestPayload(time.Now()),
|
|
modifyPayload: func(tp *testPayload) {
|
|
tp.header.Del("webhook-id")
|
|
},
|
|
expectedErr: true,
|
|
},
|
|
{
|
|
name: "missing timestamp returns error",
|
|
testPayload: newTestPayload(time.Now()),
|
|
modifyPayload: func(tp *testPayload) {
|
|
tp.header.Del("webhook-timestamp")
|
|
},
|
|
expectedErr: true,
|
|
},
|
|
{
|
|
name: "missing signature returns error",
|
|
testPayload: newTestPayload(time.Now()),
|
|
modifyPayload: func(tp *testPayload) {
|
|
tp.header.Del("webhook-signature")
|
|
},
|
|
expectedErr: true,
|
|
},
|
|
{
|
|
name: "invalid signature is invalid",
|
|
testPayload: newTestPayload(time.Now()),
|
|
modifyPayload: func(tp *testPayload) {
|
|
tp.header.Set("webhook-signature", "v1,Ceo5qEr07ixe2NLpvHk3FH9bwy/WavXrAFQ/9tdO6mc=")
|
|
},
|
|
expectedErr: true,
|
|
},
|
|
{
|
|
name: "partial signature is invalid",
|
|
testPayload: newTestPayload(time.Now()),
|
|
modifyPayload: func(tp *testPayload) {
|
|
tp.header.Set("webhook-signature", "v1,")
|
|
},
|
|
expectedErr: true,
|
|
},
|
|
{
|
|
name: "old timestamp fails",
|
|
testPayload: newTestPayload(time.Now().Add(timeTolerance * -1)),
|
|
expectedErr: true,
|
|
},
|
|
{
|
|
name: "new timestamp fails",
|
|
testPayload: newTestPayload(time.Now().Add(timeTolerance + time.Second)),
|
|
expectedErr: true,
|
|
},
|
|
{
|
|
name: "valid multi sig is valid",
|
|
testPayload: newTestPayload(time.Now()),
|
|
modifyPayload: func(tp *testPayload) {
|
|
sigs := []string{
|
|
"v1,Ceo5qEr07ixe2NLpvHk3FH9bwy/WavXrAFQ/9tdO6mc=",
|
|
"v2,Ceo5qEr07ixe2NLpvHk3FH9bwy/WavXrAFQ/9tdO6mc=",
|
|
tp.header.Get("webhook-signature"), // valid signature
|
|
"v1,Ceo5qEr07ixe2NLpvHk3FH9bwy/WavXrAFQ/9tdO6mc=",
|
|
}
|
|
tp.header.Set("webhook-signature", strings.Join(sigs, " "))
|
|
},
|
|
expectedErr: false,
|
|
},
|
|
{
|
|
name: "old timestamp passes when ignoring tolerance",
|
|
testPayload: newTestPayload(time.Now().Add(timeTolerance * -1)),
|
|
noEnforceTimestamp: true,
|
|
expectedErr: false,
|
|
},
|
|
{
|
|
name: "new timestamp passes when ignoring tolerance",
|
|
testPayload: newTestPayload(time.Now().Add(timeTolerance * 1)),
|
|
noEnforceTimestamp: true,
|
|
expectedErr: false,
|
|
},
|
|
{
|
|
name: "valid timestamp passes when ignoring tolerance",
|
|
testPayload: newTestPayload(time.Now()),
|
|
noEnforceTimestamp: true,
|
|
expectedErr: false,
|
|
},
|
|
{
|
|
name: "invalid timestamp fails when ignoring tolerance",
|
|
testPayload: newTestPayload(time.Now()),
|
|
modifyPayload: func(tp *testPayload) {
|
|
tp.header.Set("webhook-timestamp", fmt.Sprint(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC).Unix()))
|
|
},
|
|
noEnforceTimestamp: true,
|
|
expectedErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
if tc.modifyPayload != nil {
|
|
tc.modifyPayload(tc.testPayload)
|
|
}
|
|
|
|
wh, err := NewSecret(tc.testPayload.secret)
|
|
if err != nil {
|
|
t.Error(err)
|
|
continue
|
|
}
|
|
if tc.noEnforceTimestamp {
|
|
err = wh.VerifyIgnoringTimestamp(tc.testPayload.payload, tc.testPayload.header)
|
|
} else {
|
|
err = wh.Verify(tc.testPayload.payload, tc.testPayload.header)
|
|
}
|
|
if err != nil && !tc.expectedErr {
|
|
t.Errorf("%s: failed with err %s but shouldn't have", tc.name, err.Error())
|
|
} else if err == nil && tc.expectedErr {
|
|
t.Errorf("%s: didn't error but should have", tc.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWebhookPrefix(t *testing.T) {
|
|
tp := newTestPayload(time.Now())
|
|
|
|
wh, err := NewSecret(tp.secret)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = wh.Verify(tp.payload, tp.header)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
wh, err = NewSecret(fmt.Sprintf("whsec_%s", tp.secret))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = wh.Verify(tp.payload, tp.header)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestWebhookSign(t *testing.T) {
|
|
key := "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
|
|
msgID := "msg_p5jXN8AQM9LWM0D4loKWxJek"
|
|
timestamp := time.Unix(1614265330, 0)
|
|
payload := []byte(`{"test": 2432232314}`)
|
|
expected := "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE="
|
|
|
|
wh, err := NewSecret(key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
signature, err := wh.Sign(msgID, timestamp, payload)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if signature != expected {
|
|
t.Fatalf("signature %s != expected signature %s", signature, expected)
|
|
}
|
|
|
|
}
|
|
|
|
// A complete example flow for signing and verifying a webhook payload,
|
|
// including timestamp verification.
|
|
func TestSignatureFlow(t *testing.T) {
|
|
const secretKey = "MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
|
|
|
|
var (
|
|
ts = time.Now()
|
|
id = "1234567890"
|
|
)
|
|
|
|
wh, err := NewSecret(secretKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
payload := `{"type": "example.created", "timestamp":"2023-09-28T19:20:22+00:00", "data":{"str":"string","bool":true,"int":42}}`
|
|
|
|
// signing the payload with the webhook handler
|
|
signature, err := wh.Sign(id, ts, []byte(payload))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// generating the http header carrier
|
|
head := http.Header{}
|
|
head.Set(header.WebhookID, id)
|
|
head.Set(header.WebhookSignature, signature)
|
|
head.Set(header.WebhookTimestamp, fmt.Sprint(ts.Unix()))
|
|
|
|
// http request is sent to consumer
|
|
|
|
// consumer verifies the signature
|
|
err = wh.Verify([]byte(payload), head)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Done.
|
|
}
|