mirror of https://github.com/usememos/memos
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
147 lines
4.6 KiB
Go
147 lines
4.6 KiB
Go
package webhook
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestPostAsyncNilPayloadDoesNotPanic(t *testing.T) {
|
|
require.NotPanics(t, func() {
|
|
PostAsync(nil)
|
|
})
|
|
}
|
|
|
|
func TestResolveSigningKey(t *testing.T) {
|
|
rawKey := []byte("0123456789abcdef")
|
|
whsec := "whsec_" + base64.StdEncoding.EncodeToString(rawKey)
|
|
|
|
t.Run("plain secret used as-is", func(t *testing.T) {
|
|
key, err := resolveSigningKey("my-plain-secret")
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("my-plain-secret"), key)
|
|
})
|
|
|
|
t.Run("whsec_ prefix is base64-decoded", func(t *testing.T) {
|
|
key, err := resolveSigningKey(whsec)
|
|
require.NoError(t, err)
|
|
require.Equal(t, rawKey, key)
|
|
})
|
|
|
|
t.Run("whsec_ with invalid base64 fails loudly", func(t *testing.T) {
|
|
_, err := resolveSigningKey("whsec_not!valid!base64!")
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestValidateSigningSecret(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
secret string
|
|
wantErr bool
|
|
}{
|
|
{name: "empty is allowed", secret: "", wantErr: false},
|
|
{name: "printable ascii", secret: "abcDEF123!@#", wantErr: false},
|
|
{name: "valid whsec_", secret: "whsec_" + base64.StdEncoding.EncodeToString([]byte("key")), wantErr: false},
|
|
{name: "newline rejected", secret: "abc\ndef", wantErr: true},
|
|
{name: "carriage return rejected", secret: "abc\rdef", wantErr: true},
|
|
{name: "tab rejected", secret: "abc\tdef", wantErr: true},
|
|
{name: "non-ascii rejected", secret: "abc€def", wantErr: true},
|
|
{name: "whsec_ with invalid base64 rejected", secret: "whsec_not!base64", wantErr: true},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := ValidateSigningSecret(tc.secret)
|
|
if tc.wantErr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPostSignsRequest verifies the end-to-end Standard Webhooks signature so a
|
|
// receiver following the documented verification recipe will accept our requests.
|
|
func TestPostSignsRequest(t *testing.T) {
|
|
// httptest listens on 127.0.0.1, which the SSRF guard blocks by default.
|
|
prev := AllowPrivateIPs
|
|
AllowPrivateIPs = true
|
|
defer func() { AllowPrivateIPs = prev }()
|
|
|
|
rawKey := []byte("0123456789abcdef0123456789abcdef")
|
|
cases := []struct {
|
|
name string
|
|
secret string
|
|
key []byte
|
|
}{
|
|
{name: "plain secret", secret: "plain-secret-value", key: []byte("plain-secret-value")},
|
|
{name: "whsec_ secret", secret: "whsec_" + base64.StdEncoding.EncodeToString(rawKey), key: rawKey},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var gotID, gotTimestamp, gotSignature string
|
|
var gotBody []byte
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotID = r.Header.Get("webhook-id")
|
|
gotTimestamp = r.Header.Get("webhook-timestamp")
|
|
gotSignature = r.Header.Get("webhook-signature")
|
|
gotBody, _ = io.ReadAll(r.Body)
|
|
_, _ = w.Write([]byte(`{"code":0}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
err := Post(&WebhookRequestPayload{
|
|
URL: server.URL,
|
|
ActivityType: "memos.memo.created",
|
|
Creator: "users/1",
|
|
SigningSecret: tc.secret,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, strings.HasPrefix(gotID, "msg_"), "webhook-id should be prefixed with msg_")
|
|
require.NotEmpty(t, gotTimestamp)
|
|
require.True(t, strings.HasPrefix(gotSignature, "v1,"), "signature should carry the v1 version tag")
|
|
|
|
// Recompute the signature the way a receiver would and confirm it matches.
|
|
mac := hmac.New(sha256.New, tc.key)
|
|
mac.Write([]byte(gotID + "." + gotTimestamp + "."))
|
|
mac.Write(gotBody)
|
|
want := base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
|
require.Equal(t, "v1,"+want, gotSignature)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPostWithoutSecretSetsNoSignatureHeaders ensures unsigned webhooks stay unsigned.
|
|
func TestPostWithoutSecretSetsNoSignatureHeaders(t *testing.T) {
|
|
prev := AllowPrivateIPs
|
|
AllowPrivateIPs = true
|
|
defer func() { AllowPrivateIPs = prev }()
|
|
|
|
var hasSignatureHeaders bool
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
hasSignatureHeaders = r.Header.Get("webhook-id") != "" ||
|
|
r.Header.Get("webhook-timestamp") != "" ||
|
|
r.Header.Get("webhook-signature") != ""
|
|
_, _ = w.Write([]byte(`{"code":0}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
err := Post(&WebhookRequestPayload{
|
|
URL: server.URL,
|
|
ActivityType: "memos.memo.created",
|
|
Creator: "users/1",
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, hasSignatureHeaders, "no signature headers should be set when no secret is configured")
|
|
}
|