This commit is contained in:
Ian Roberts 2024-12-09 22:51:28 +08:00 committed by GitHub
commit 1aa4fd0ca8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 542 additions and 311 deletions

View file

@ -25,7 +25,7 @@ although the examples on this page all use the JSON format.
## Incoming Github webhook
This example works on 2.8+ versions of Webhook - if you are on a previous series, change `payload-hmac-sha1` to `payload-hash-sha1`.
This example works on 2.9+ versions of Webhook - if you are on a previous series, change the `check-signature` block to an equivalent `match` block, see the [Hook-Rules](Hook-Rules.md#legacy-match-rules-for-signatures) page for full details.
```json
[
@ -53,11 +53,11 @@ This example works on 2.8+ versions of Webhook - if you are on a previous series
"and":
[
{
"match":
"check-signature":
{
"type": "payload-hmac-sha1",
"algorithm": "sha1",
"secret": "mysecret",
"parameter":
"signature":
{
"source": "header",
"name": "X-Hub-Signature"
@ -185,11 +185,11 @@ Values in the request body can be accessed in the command or to the match rule b
"and":
[
{
"match":
"check-signature":
{
"type": "payload-hmac-sha256",
"algorithm": "sha256",
"secret": "mysecret",
"parameter":
"signature":
{
"source": "header",
"name": "X-Gogs-Signature"

View file

@ -9,11 +9,12 @@
* [Match](#match)
* [Match value](#match-value)
* [Match regex](#match-regex)
* [Match Whitelisted IP range](#match-whitelisted-ip-range)
* [Match scalr-signature](#match-scalr-signature)
* [Check signature](#check-signature)
* [Match payload-hmac-sha1](#match-payload-hmac-sha1)
* [Match payload-hmac-sha256](#match-payload-hmac-sha256)
* [Match payload-hmac-sha512](#match-payload-hmac-sha512)
* [Match Whitelisted IP range](#match-whitelisted-ip-range)
* [Match scalr-signature](#match-scalr-signature)
## And
*And rule* will evaluate to _true_, if and only if all of the sub rules evaluate to _true_.
@ -183,78 +184,6 @@ For the regex syntax, check out <http://golang.org/pkg/regexp/syntax/>
}
```
### Match payload-hmac-sha1
Validate the HMAC of the payload using the SHA1 hash and the given *secret*.
```json
{
"match":
{
"type": "payload-hmac-sha1",
"secret": "yoursecret",
"parameter":
{
"source": "header",
"name": "X-Hub-Signature"
}
}
}
```
Note that if multiple signatures were passed via a comma separated string, each
will be tried unless a match is found. For example:
```
X-Hub-Signature: sha1=the-first-signature,sha1=the-second-signature
```
### Match payload-hmac-sha256
Validate the HMAC of the payload using the SHA256 hash and the given *secret*.
```json
{
"match":
{
"type": "payload-hmac-sha256",
"secret": "yoursecret",
"parameter":
{
"source": "header",
"name": "X-Signature"
}
}
}
```
Note that if multiple signatures were passed via a comma separated string, each
will be tried unless a match is found. For example:
```
X-Hub-Signature: sha256=the-first-signature,sha256=the-second-signature
```
### Match payload-hmac-sha512
Validate the HMAC of the payload using the SHA512 hash and the given *secret*.
```json
{
"match":
{
"type": "payload-hmac-sha512",
"secret": "yoursecret",
"parameter":
{
"source": "header",
"name": "X-Signature"
}
}
}
```
Note that if multiple signatures were passed via a comma separated string, each
will be tried unless a match is found. For example:
```
X-Hub-Signature: sha512=the-first-signature,sha512=the-second-signature
```
### Match Whitelisted IP range
The IP can be IPv4- or IPv6-formatted, using [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_blocks). To match a single IP address only, use `/32`.
@ -286,3 +215,89 @@ Given the time check make sure that NTP is enabled on both your Scalr and webhoo
}
}
```
## Check Signature
Many webhook protocols involve the hook sender computing an [HMAC](https://en.wikipedia.org/wiki/HMAC) _signature_ over the request content using a shared secret key, and sending the expected signature value as part of the webhook call. The webhook recipient can then compute their own value for the signature using the same secret key and verify that value against the one supplied by the sender. Since the sender and receiver are (or at least _should be_) the only parties that have knowledge of the secret, a matching signature guarantees that the payload is valid and was created by the legitimate sender.
The `"check-signature"` rule type is used to validate these kinds of signatures. In its simplest form you just specify the _algorithm_ (`sha1`, `sha256` or `sha512`), the _secret_, and where in the request to find the signature (typically a header or a query parameter). Webhook will compute the HMAC over the whole of the request body using the supplied secret, and compare the result to the one taken from the request
```json
{
"check-signature":
{
"algorithm": "sha256",
"secret": "yoursecret",
"signature":
{
"source": "header",
"name": "X-Hub-Signature"
}
}
}
```
Note that if multiple signatures were passed via a comma separated string, each
will be tried unless a match is found, and any `algorithm=` prefix is stripped off
each signature value before comparison. This allows for cases where the sender includes
several signatures with different algorithms in the same header, e.g.:
```
X-Hub-Signature: sha1=the-sha1-signature,sha256=the-sha256-signature
```
If the sender computes the signature over something other than just the request body then you can optionally provide a `"string-to-sign"` argument. Usually this will be a template that assembles the string-to-sign from different parts of the request (one of which could be the body). For example this would compute a signature over the values of the `X-Request-Id` header, `Date` header, and request body, separated by line breaks:
```yaml
check-signature:
algorithm: sha512
secret: 5uper5eecret
signature:
source: header
name: X-Hook-Signature
string-to-sign:
source: template
name: |
{{- printf "%s\r\n" (.GetHeader "x-request-id") -}}
{{- printf "%s\r\n" (.GetHeader "date") -}}
{{- .BodyText -}}
```
Note that signature algorithms can be very particular about whether "line breaks" are unix style LF or Windows-style CR+LF. It is safest to be explicit, as in the above example, using `{{- -}}` blocks (that ignore the white space within the template itself either side of the block) and `printf` with `\n` or `\r\n`, to ensure the template generates the correct style of line endings whatever platform you created it on.
### Legacy "match" rules for signatures
In previous versions of webhook signature verification was handled by a set of specific "match" rule types named `payload-hmac-<algorithm>` - the legacy format is still understood but you may wish to update your existing configurations to the new format.
The legacy configuration
```json
{
"match":
{
"type": "payload-hmac-<type>",
"secret": "secret",
"parameter":
{
"source": "header",
"name": "X-Signature"
}
}
}
```
is equivalent to the new style
```json
{
"check-signature":
{
"algorithm": "<type>",
"secret": "secret",
"signature":
{
"source": "header",
"name": "X-Signature"
}
}
}
```

View file

@ -132,3 +132,79 @@ and for query variables you can use
"source": "entire-query"
}
```
# Using a template
If the above source types do not provide sufficient flexibility for your needs, it is possible to provide a [Go template][tt] to compute the value. The template _context_ provides access to the headers, query parameters, parsed payload, and the complete request body content. For clarity, the following examples show the YAML form of the definition rather than JSON, since template strings will often contain double quotes, line breaks, etc. that need to be specially encoded in JSON.
## Examples
Extract a value from the payload, if it is present, otherwise from the query string (this allows for a hook that may be called with either a POST request with the form data in the payload, or a GET request with the same data in the URL):
```yaml
- source: template
name: |-
{{- with .Payload.requestId -}}
{{- . -}}
{{- else -}}
{{- .Query.requestId -}}
{{- end -}}
```
Given the following JSON payload describing multiple commits:
```json
{
"commits": [
{
"commit": {
"commit-id": 1
}
}, {
"commit": {
"commit-id": 2
}
}
]
}
```
this template would generate a semicolon-separated list of all the commit IDs:
```yaml
- source: template
name: |-
{{- range $i, $c := .Payload.commits -}}
{{- if gt $i 0 -}};{{- end -}}
{{- index $c.commit "commit-id" -}}
{{- end -}}
```
Here `.Payload.commits` is the array of objects, each of these has a field `commit`, which in turn has a field `commit-id`. The `range` operator iterates over the commits array, setting `$i` to the (zero-based) index and `$c` to the object. The template then prints a semicolon if this is not the first iteration, then we extract the `commit` field from that object, then in turn the `commit-id`. Note how the first level can be extracted with just `$c.commit` because the field name is a valid identifier, but for the second level we must use the `index` function.
To access request _header_ values, use the `.GetHeader` function:
```yaml
- source: template
name: |-
{{- .GetHeader "x-request-id" }}:{{ index .Query "app-id" -}}
```
## Template context
The following items are available to templates, in addition to the [standard functions](https://pkg.go.dev/text/template#hdr-Functions) provided by Go:
- `.Payload` - the parsed request payload, which may be JSON, XML or form data.
- `.Query` - the query string parameters from the hook URL.
- `.GetHeader "header-name"` - function that returns the value of the given request header, case-insensitive
- `.ContentType` - the request content type
- `.ID` - request ID assigned by webhook itself
- `.Method` - the HTTP request method (`GET`, `POST`, etc.)
- `.RemoteAddr` - IP address of the client (though this may not be accurate if webhook is behind a [reverse proxy](Hook-Rules.md#match-whitelisted-ip-range))
- `.BodyText` - the complete raw content of the request body, as a string
The following are also available but less frequently needed:
- `.Body` - complete body content, but as a slice of bytes rather than as a string
- `.Headers` - the map of HTTP headers. Useful if you need to `range` over the headers, but to look up keys directly in this map you must use the canonical form - the `.GetHeader` function performs a case-insensitive lookup.
[tt]: https://golang.org/pkg/text/template/

View file

@ -73,5 +73,33 @@ Additionally, the result is piped through the built-in Go template function `js`
```
## Changing the template delimiters
If your hook configuration includes lookup arguments of type `{"source": "template"}`, and you also need to parse the hooks file _as_ a template, you can use the `-template-delims` parameter to change the template delimiter used when processing the hook file so it does not clash with the standard `{{ ... }}` delimiters used for template lookups. The parameter is a comma-separated pair of the left and right delimiter strings, e.g. `-template-delims='[[,]]'` would use square brackets. For a configuration like this:
```json
[
{
"id": "example",
"trigger-rule": {
"check-signature": {
"algorithm": "sha256",
"secret": "[[ getenv `XXXTEST_SECRET` | js ]]",
"signature": {
"source": "header",
"name": "X-Signature"
},
"string-to-sign": {
"source": "template",
"name": "{{ .BodyText }}{{ .GetHeader `date` }}"
}
}
}
}
]
```
the `-template-delims='[[,]]'` setting would cause the `getenv` part to be interpreted when parsing the hook file, whereas the string-to-sign template would be executed when evaluating the trigger rule against each request.
[w]: https://github.com/adnanh/webhook
[tt]: https://golang.org/pkg/text/template/

View file

@ -41,6 +41,7 @@ const (
SourceEntirePayload string = "entire-payload"
SourceEntireQuery string = "entire-query"
SourceEntireHeaders string = "entire-headers"
SourceTemplate string = "template"
)
const (
@ -195,45 +196,6 @@ func ValidateMAC(payload []byte, mac hash.Hash, signatures []string) (string, er
return actualMAC, e
}
// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
func CheckPayloadSignature(payload []byte, secret, signature string) (string, error) {
if secret == "" {
return "", errors.New("signature validation secret can not be empty")
}
// Extract the signatures.
signatures := ExtractSignatures(signature, "sha1=")
// Validate the MAC.
return ValidateMAC(payload, hmac.New(sha1.New, []byte(secret)), signatures)
}
// CheckPayloadSignature256 calculates and verifies SHA256 signature of the given payload
func CheckPayloadSignature256(payload []byte, secret, signature string) (string, error) {
if secret == "" {
return "", errors.New("signature validation secret can not be empty")
}
// Extract the signatures.
signatures := ExtractSignatures(signature, "sha256=")
// Validate the MAC.
return ValidateMAC(payload, hmac.New(sha256.New, []byte(secret)), signatures)
}
// CheckPayloadSignature512 calculates and verifies SHA512 signature of the given payload
func CheckPayloadSignature512(payload []byte, secret, signature string) (string, error) {
if secret == "" {
return "", errors.New("signature validation secret can not be empty")
}
// Extract the signatures.
signatures := ExtractSignatures(signature, "sha512=")
// Validate the MAC.
return ValidateMAC(payload, hmac.New(sha512.New, []byte(secret)), signatures)
}
func CheckScalrSignature(r *Request, signingKey string, checkDate bool) (bool, error) {
if r.Headers == nil {
return false, nil
@ -438,6 +400,76 @@ type Argument struct {
Name string `json:"name,omitempty"`
EnvName string `json:"envname,omitempty"`
Base64Decode bool `json:"base64decode,omitempty"`
// if the Argument is SourceTemplate, this will be the compiled template,
// otherwise it will be nil
template *template.Template
}
// UnmarshalJSON parses an Argument in the normal way, and then allows the
// newly-loaded Argument to do any necessary post-processing.
func (ha *Argument) UnmarshalJSON(text []byte) error {
// First unmarshal as normal, skipping the custom unmarshaller
type jsonArgument Argument
if err := json.Unmarshal(text, (*jsonArgument)(ha)); err != nil {
return err
}
return ha.postProcess()
}
// postProcess does the necessary post-unmarshal processing for this argument.
// If the argument is a SourceTemplate it compiles the template string into an
// executable template. This method is idempotent, i.e. it is safe to call
// more than once on the same Argument
func (ha *Argument) postProcess() error {
if ha.Source == SourceTemplate && ha.template == nil {
// now compile the template
var err error
ha.template, err = template.New("argument").Option("missingkey=zero").Parse(ha.Name)
return err
}
return nil
}
// templateContext is the context passed as "." to the template executed when
// getting an Argument of type SourceTemplate
type templateContext struct {
ID string
ContentType string
Body []byte
Headers map[string]interface{}
Query map[string]interface{}
Payload map[string]interface{}
Method string
RemoteAddr string
}
// BodyText is a convenience to access the request Body as a string. This means
// you can just say {{ .BodyText }} instead of having to do a trick like
// {{ printf "%s" .Body }}
func (ctx *templateContext) BodyText() string {
return string(ctx.Body)
}
// GetHeader is a function to fetch a specific item out of the headers map
// by its case insensitive name. The header name is converted to canonical form
// before being looked up in the header map, e.g. {{ .GetHeader "x-request-id" }}
func (ctx *templateContext) GetHeader(name string) interface{} {
return ctx.Headers[textproto.CanonicalMIMEHeaderKey(name)]
}
func (ha *Argument) runTemplate(r *Request) (string, error) {
w := &strings.Builder{}
ctx := &templateContext{
r.ID, r.ContentType, r.Body, r.Headers, r.Query, r.Payload, r.RawRequest.Method, r.RawRequest.RemoteAddr,
}
err := ha.template.Execute(w, ctx)
if err == nil {
return w.String(), nil
}
return "", err
}
// Get Argument method returns the value for the Argument's key name
@ -500,6 +532,9 @@ func (ha *Argument) Get(r *Request) (string, error) {
}
return string(res), nil
case SourceTemplate:
return ha.runTemplate(r)
}
if source != nil {
@ -744,7 +779,9 @@ type Hooks []Hook
// LoadFromFile attempts to load hooks from the specified file, which
// can be either JSON or YAML. The asTemplate parameter causes the file
// contents to be parsed as a Go text/template prior to unmarshalling.
func (h *Hooks) LoadFromFile(path string, asTemplate bool) error {
// The delimsStr parameter is a comma-separated pair of the left and right
// template delimiters, or an empty string to use the default '{{,}}'.
func (h *Hooks) LoadFromFile(path string, asTemplate bool, delimsStr string) error {
if path == "" {
return nil
}
@ -758,8 +795,12 @@ func (h *Hooks) LoadFromFile(path string, asTemplate bool) error {
if asTemplate {
funcMap := template.FuncMap{"getenv": getenv}
left, right, found := strings.Cut(delimsStr, ",")
if !found && delimsStr != "" {
return fmt.Errorf("invalid delimiters %q - should be left and right delimiters separated by a comma", delimsStr)
}
tmpl, err := template.New("hooks").Funcs(funcMap).Parse(string(file))
tmpl, err := template.New("hooks").Funcs(funcMap).Delims(strings.TrimSpace(left), strings.TrimSpace(right)).Parse(string(file))
if err != nil {
return err
}
@ -774,7 +815,12 @@ func (h *Hooks) LoadFromFile(path string, asTemplate bool) error {
file = buf.Bytes()
}
return yaml.Unmarshal(file, h)
err := yaml.Unmarshal(file, h)
if err != nil {
return err
}
return h.postProcess()
}
// Append appends hooks unless the new hooks contain a hook with an ID that already exists
@ -802,12 +848,81 @@ func (h *Hooks) Match(id string) *Hook {
return nil
}
func (h *Hooks) postProcess() error {
for i := range *h {
rules := (*h)[i].TriggerRule
if rules != nil {
if err := postProcess(rules); err != nil {
return err
}
}
}
return nil
}
// Rules is a structure that contains one of the valid rule types
type Rules struct {
And *AndRule `json:"and,omitempty"`
Or *OrRule `json:"or,omitempty"`
Not *NotRule `json:"not,omitempty"`
Match *MatchRule `json:"match,omitempty"`
And *AndRule `json:"and,omitempty"`
Or *OrRule `json:"or,omitempty"`
Not *NotRule `json:"not,omitempty"`
Match *MatchRule `json:"match,omitempty"`
Signature *SignatureRule `json:"check-signature,omitempty"`
}
// postProcess is called on each Rules instance after loading it from JSON/YAML,
// to replace any legacy constructs with their modern equivalents.
func postProcess(r *Rules) error {
if r.And != nil {
for i := range *(r.And) {
if err := postProcess(&(*r.And)[i]); err != nil {
return err
}
}
}
if r.Or != nil {
for i := range *(r.Or) {
if err := postProcess(&(*r.Or)[i]); err != nil {
return err
}
}
}
if r.Not != nil {
return postProcess((*Rules)(r.Not))
}
if r.Match != nil {
// convert any signature matching rules to the equivalent SignatureRule
if r.Match.Type == MatchHashSHA1 || r.Match.Type == MatchHMACSHA1 {
log.Printf(`warn: use of deprecated match type %s; use a check-signature rule instead`, r.Match.Type)
r.Signature = &SignatureRule{
Algorithm: AlgorithmSHA1,
Secret: r.Match.Secret,
Signature: r.Match.Parameter,
}
r.Match = nil
return nil
}
if r.Match.Type == MatchHashSHA256 || r.Match.Type == MatchHMACSHA256 {
log.Printf(`warn: use of deprecated match type %s; use a check-signature rule instead`, r.Match.Type)
r.Signature = &SignatureRule{
Algorithm: AlgorithmSHA256,
Secret: r.Match.Secret,
Signature: r.Match.Parameter,
}
r.Match = nil
return nil
}
if r.Match.Type == MatchHashSHA512 || r.Match.Type == MatchHMACSHA512 {
log.Printf(`warn: use of deprecated match type %s; use a check-signature rule instead`, r.Match.Type)
r.Signature = &SignatureRule{
Algorithm: AlgorithmSHA512,
Secret: r.Match.Secret,
Signature: r.Match.Parameter,
}
r.Match = nil
return nil
}
}
return nil
}
// Evaluate finds the first rule property that is not nil and returns the value
@ -822,6 +937,8 @@ func (r Rules) Evaluate(req *Request) (bool, error) {
return r.Not.Evaluate(req)
case r.Match != nil:
return r.Match.Evaluate(req)
case r.Signature != nil:
return r.Signature.Evaluate(req)
}
return false, nil
@ -896,16 +1013,19 @@ type MatchRule struct {
// Constants for the MatchRule type
const (
MatchValue string = "value"
MatchRegex string = "regex"
MatchValue string = "value"
MatchRegex string = "regex"
IPWhitelist string = "ip-whitelist"
ScalrSignature string = "scalr-signature"
// legacy match types that have migrated to SignatureRule
MatchHMACSHA1 string = "payload-hmac-sha1"
MatchHMACSHA256 string = "payload-hmac-sha256"
MatchHMACSHA512 string = "payload-hmac-sha512"
MatchHashSHA1 string = "payload-hash-sha1"
MatchHashSHA256 string = "payload-hash-sha256"
MatchHashSHA512 string = "payload-hash-sha512"
IPWhitelist string = "ip-whitelist"
ScalrSignature string = "scalr-signature"
)
// Evaluate MatchRule will return based on the type
@ -924,29 +1044,74 @@ func (r MatchRule) Evaluate(req *Request) (bool, error) {
return compare(arg, r.Value), nil
case MatchRegex:
return regexp.MatchString(r.Regex, arg)
case MatchHashSHA1:
log.Print(`warn: use of deprecated option payload-hash-sha1; use payload-hmac-sha1 instead`)
fallthrough
case MatchHMACSHA1:
_, err := CheckPayloadSignature(req.Body, r.Secret, arg)
return err == nil, err
case MatchHashSHA256:
log.Print(`warn: use of deprecated option payload-hash-sha256: use payload-hmac-sha256 instead`)
fallthrough
case MatchHMACSHA256:
_, err := CheckPayloadSignature256(req.Body, r.Secret, arg)
return err == nil, err
case MatchHashSHA512:
log.Print(`warn: use of deprecated option payload-hash-sha512: use payload-hmac-sha512 instead`)
fallthrough
case MatchHMACSHA512:
_, err := CheckPayloadSignature512(req.Body, r.Secret, arg)
return err == nil, err
}
}
return false, err
}
type SignatureRule struct {
Algorithm string `json:"algorithm,omitempty"`
Secret string `json:"secret,omitempty"`
Signature Argument `json:"signature,omitempty"`
Prefix string `json:"prefix,omitempty"`
StringToSign *Argument `json:"string-to-sign,omitempty"`
}
// Constants for the SignatureRule type
const (
AlgorithmSHA1 string = "sha1"
AlgorithmSHA256 string = "sha256"
AlgorithmSHA512 string = "sha512"
)
// Evaluate extracts the signature payload and signature value from the request
// and checks whether the signature matches
func (r SignatureRule) Evaluate(req *Request) (bool, error) {
if r.Secret == "" {
return false, errors.New("signature validation secret can not be empty")
}
var hashConstructor func() hash.Hash
switch r.Algorithm {
case AlgorithmSHA1:
hashConstructor = sha1.New
case AlgorithmSHA256:
hashConstructor = sha256.New
case AlgorithmSHA512:
hashConstructor = sha512.New
default:
return false, fmt.Errorf("unknown hash algorithm %s", r.Algorithm)
}
prefix := r.Prefix
if prefix == "" {
// default prefix is "sha1=" for SHA1, etc.
prefix = fmt.Sprintf("%s=", r.Algorithm)
}
// find the signature
sig, err := r.Signature.Get(req)
if err != nil {
return false, err
}
// determine the payload that is signed
payload := req.Body
if r.StringToSign != nil {
payloadStr, err := r.StringToSign.Get(req)
if err != nil {
return false, fmt.Errorf("could not build string-to-sign: %w", err)
}
payload = []byte(payloadStr)
}
// check the signature
signatures := ExtractSignatures(sig, prefix)
_, err = ValidateMAC(payload, hmac.New(hashConstructor, []byte(r.Secret)), signatures)
return err == nil, err
}
// compare is a helper function for constant time string comparisons.
func compare(a, b string) bool {
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1

View file

@ -40,100 +40,6 @@ func TestGetParameter(t *testing.T) {
}
}
var checkPayloadSignatureTests = []struct {
payload []byte
secret string
signature string
mac string
ok bool
}{
{[]byte(`{"a": "z"}`), "secret", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
{[]byte(`{"a": "z"}`), "secret", "sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e,sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
{[]byte(``), "secret", "25af6174a0fcecc4d346680a72b7ce644b9a88e8", "25af6174a0fcecc4d346680a72b7ce644b9a88e8", true},
// failures
{[]byte(`{"a": "z"}`), "secret", "XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e,sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
{[]byte(`{"a": "z"}`), "secreX", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "900225703e9342328db7307692736e2f7cc7b36e", false},
{[]byte(`{"a": "z"}`), "", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "", false},
{[]byte(``), "secret", "XXXf6174a0fcecc4d346680a72b7ce644b9a88e8", "25af6174a0fcecc4d346680a72b7ce644b9a88e8", false},
}
func TestCheckPayloadSignature(t *testing.T) {
for _, tt := range checkPayloadSignatureTests {
mac, err := CheckPayloadSignature(tt.payload, tt.secret, tt.signature)
if (err == nil) != tt.ok || mac != tt.mac {
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
}
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
t.Errorf("error message should not disclose expected mac: %s", err)
}
}
}
var checkPayloadSignature256Tests = []struct {
payload []byte
secret string
signature string
mac string
ok bool
}{
{[]byte(`{"a": "z"}`), "secret", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
{[]byte(`{"a": "z"}`), "secret", "sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89,sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
{[]byte(``), "secret", "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", true},
// failures
{[]byte(`{"a": "z"}`), "secret", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89,sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
{[]byte(`{"a": "z"}`), "", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "", false},
{[]byte(``), "secret", "XXX66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", false},
}
func TestCheckPayloadSignature256(t *testing.T) {
for _, tt := range checkPayloadSignature256Tests {
mac, err := CheckPayloadSignature256(tt.payload, tt.secret, tt.signature)
if (err == nil) != tt.ok || mac != tt.mac {
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
}
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
t.Errorf("error message should not disclose expected mac: %s", err)
}
}
}
var checkPayloadSignature512Tests = []struct {
payload []byte
secret string
signature string
mac string
ok bool
}{
{[]byte(`{"a": "z"}`), "secret", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", true},
{[]byte(`{"a": "z"}`), "secret", "sha512=4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", true},
{[]byte(``), "secret", "b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", "b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", true},
// failures
{[]byte(`{"a": "z"}`), "secret", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", false},
{[]byte(`{"a": "z"}`), "", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "", false},
{[]byte(``), "secret", "XXX9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", "b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", false},
}
func TestCheckPayloadSignature512(t *testing.T) {
for _, tt := range checkPayloadSignature512Tests {
mac, err := CheckPayloadSignature512(tt.payload, tt.secret, tt.signature)
if (err == nil) != tt.ok || mac != tt.mac {
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
}
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
t.Errorf("error message should not disclose expected mac: %s", err)
}
}
}
var checkScalrSignatureTests = []struct {
description string
headers map[string]interface{}
@ -274,7 +180,7 @@ var argumentGetTests = []struct {
func TestArgumentGet(t *testing.T) {
for _, tt := range argumentGetTests {
a := Argument{tt.source, tt.name, "", false}
a := Argument{tt.source, tt.name, "", false, nil}
r := &Request{
Headers: tt.headers,
Query: tt.query,
@ -294,14 +200,14 @@ var hookParseJSONParametersTests = []struct {
rheaders, rquery, rpayload map[string]interface{}
ok bool
}{
{[]Argument{Argument{"header", "a", "", false}}, map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, true},
{[]Argument{Argument{"url", "a", "", false}}, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
{[]Argument{Argument{"payload", "a", "", false}}, nil, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
{[]Argument{Argument{"header", "z", "", false}}, map[string]interface{}{"Z": `{}`}, nil, nil, map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, true},
{[]Argument{Argument{"header", "a", "", false, nil}}, map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, true},
{[]Argument{Argument{"url", "a", "", false, nil}}, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
{[]Argument{Argument{"payload", "a", "", false, nil}}, nil, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
{[]Argument{Argument{"header", "z", "", false, nil}}, map[string]interface{}{"Z": `{}`}, nil, nil, map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, true},
// failures
{[]Argument{Argument{"header", "z", "", false}}, map[string]interface{}{"Z": ``}, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, false}, // empty string
{[]Argument{Argument{"header", "y", "", false}}, map[string]interface{}{"X": `{}`}, nil, nil, map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter
{[]Argument{Argument{"string", "z", "", false}}, map[string]interface{}{"Z": ``}, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, false}, // invalid argument source
{[]Argument{Argument{"header", "z", "", false, nil}}, map[string]interface{}{"Z": ``}, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, false}, // empty string
{[]Argument{Argument{"header", "y", "", false, nil}}, map[string]interface{}{"X": `{}`}, nil, nil, map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter
{[]Argument{Argument{"string", "z", "", false, nil}}, map[string]interface{}{"Z": ``}, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, false}, // invalid argument source
}
func TestHookParseJSONParameters(t *testing.T) {
@ -326,9 +232,9 @@ var hookExtractCommandArgumentsTests = []struct {
value []string
ok bool
}{
{"test", []Argument{Argument{"header", "a", "", false}}, map[string]interface{}{"A": "z"}, nil, nil, []string{"test", "z"}, true},
{"test", []Argument{Argument{"header", "a", "", false, nil}}, map[string]interface{}{"A": "z"}, nil, nil, []string{"test", "z"}, true},
// failures
{"fail", []Argument{Argument{"payload", "a", "", false}}, map[string]interface{}{"A": "z"}, nil, nil, []string{"fail", ""}, false},
{"fail", []Argument{Argument{"payload", "a", "", false, nil}}, map[string]interface{}{"A": "z"}, nil, nil, []string{"fail", ""}, false},
}
func TestHookExtractCommandArguments(t *testing.T) {
@ -351,20 +257,21 @@ func TestHookExtractCommandArguments(t *testing.T) {
// we test both cases where the name of the data is used as the name of the
// env key & the case where the hook definition sets the env var name to a
// fixed value using the envname construct like so::
// [
// {
// "id": "push",
// "execute-command": "bb2mm",
// "command-working-directory": "/tmp",
// "pass-environment-to-command":
// [
// {
// "source": "entire-payload",
// "envname": "PAYLOAD"
// },
// ]
// }
// ]
//
// [
// {
// "id": "push",
// "execute-command": "bb2mm",
// "command-working-directory": "/tmp",
// "pass-environment-to-command":
// [
// {
// "source": "entire-payload",
// "envname": "PAYLOAD"
// },
// ]
// }
// ]
var hookExtractCommandArgumentsForEnvTests = []struct {
exec string
args []Argument
@ -375,14 +282,14 @@ var hookExtractCommandArgumentsForEnvTests = []struct {
// successes
{
"test",
[]Argument{Argument{"header", "a", "", false}},
[]Argument{Argument{"header", "a", "", false, nil}},
map[string]interface{}{"A": "z"}, nil, nil,
[]string{"HOOK_a=z"},
true,
},
{
"test",
[]Argument{Argument{"header", "a", "MYKEY", false}},
[]Argument{Argument{"header", "a", "MYKEY", false, nil}},
map[string]interface{}{"A": "z"}, nil, nil,
[]string{"MYKEY=z"},
true,
@ -390,7 +297,7 @@ var hookExtractCommandArgumentsForEnvTests = []struct {
// failures
{
"fail",
[]Argument{Argument{"payload", "a", "", false}},
[]Argument{Argument{"payload", "a", "", false, nil}},
map[string]interface{}{"A": "z"}, nil, nil,
[]string{},
false,
@ -432,7 +339,7 @@ func TestHooksLoadFromFile(t *testing.T) {
for _, tt := range hooksLoadFromFileTests {
h := &Hooks{}
err := h.LoadFromFile(tt.path, tt.asTemplate)
err := h.LoadFromFile(tt.path, tt.asTemplate, "")
if (err == nil) != tt.ok {
t.Errorf(err.Error())
}
@ -449,13 +356,13 @@ func TestHooksTemplateLoadFromFile(t *testing.T) {
}
h := &Hooks{}
err := h.LoadFromFile(tt.path, tt.asTemplate)
err := h.LoadFromFile(tt.path, tt.asTemplate, "")
if (err == nil) != tt.ok {
t.Errorf(err.Error())
continue
}
s := (*h.Match("webhook").TriggerRule.And)[0].Match.Secret
s := (*h.Match("webhook").TriggerRule.And)[0].Signature.Secret
if s != secret {
t.Errorf("Expected secret of %q, got %q", secret, s)
}
@ -489,24 +396,14 @@ var matchRuleTests = []struct {
ok bool
err bool
}{
{"value", "", "", "z", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
{"regex", "^z", "", "z", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
{"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
{"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
{"value", "", "", "z", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
{"regex", "^z", "", "z", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
// failures
{"value", "", "", "X", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
{"regex", "^X", "", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
{"value", "", "2", "X", "", Argument{"header", "a", "", false}, map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, true}, // reference invalid header
{"value", "", "", "X", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
{"regex", "^X", "", "", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
{"value", "", "2", "X", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, true}, // reference invalid header
// errors
{"regex", "*", "", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex
{"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"payload-hmac-sha512", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"payload-hash-sha512", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"regex", "*", "", "", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex
// IP whitelisting, valid cases
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
@ -541,6 +438,55 @@ func TestMatchRule(t *testing.T) {
}
}
var signatureRuleTests = []struct {
algorithm, secret string
sigSource Argument
stringToSign *Argument
headers, query, payload map[string]interface{}
body []byte
ok bool
err bool
}{
{"sha1", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), true, false},
{"sha1", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), true, false},
{"sha256", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), true, false},
{"sha256", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), true, false},
// errors
{"sha1", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": ""}, nil, nil, []byte{}, false, true}, // invalid hmac
{"sha1", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": ""}, nil, nil, []byte{}, false, true}, // invalid hmac
{"sha256", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": ""}, nil, nil, []byte{}, false, true}, // invalid hmac
{"sha256", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": ""}, nil, nil, []byte{}, false, true}, // invalid hmac
{"sha512", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": ""}, nil, nil, []byte{}, false, true}, // invalid hmac
{"sha512", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": ""}, nil, nil, []byte{}, false, true}, // invalid hmac
// template to build custom string-to-sign
{"sha256", "secret", Argument{"header", "a", "", false, nil}, &Argument{"template", "{{ printf \"%s\\n%s\" .BodyText (.GetHeader \"x-id\") }}", "", false, nil}, map[string]interface{}{"A": "sha256=4f1d62e6e6de1e31537a5faefabeffd7dce115bc499584feefbf8db6d2da4027", "X-Id": "test"}, nil, nil, []byte(`{"a": "z"}`), true, false},
{"sha256", "secret", Argument{"header", "a", "", false, nil}, &Argument{"template", "{{ printf \"%s\\n%s\" .BodyText (.GetHeader \"x-id\") }}", "", false, nil}, map[string]interface{}{"A": "sha256=4f1d62e6e6de1e31537a5faefabeffd7dce115bc499584feefbf8db6d2da4027", "X-Id": "unexpected"}, nil, nil, []byte(`{"a": "z"}`), false, true},
}
func TestSignatureRule(t *testing.T) {
for i, tt := range signatureRuleTests {
if tt.stringToSign != nil {
// post process the argument, as it would have been if it were loaded from a hooks file
tt.stringToSign.postProcess()
}
r := SignatureRule{tt.algorithm, tt.secret, tt.sigSource, "", tt.stringToSign}
req := &Request{
Headers: tt.headers,
Query: tt.query,
Payload: tt.payload,
Body: tt.body,
RawRequest: &http.Request{
RemoteAddr: "",
},
}
ok, err := r.Evaluate(req)
if ok != tt.ok || (err != nil) != tt.err {
t.Errorf("%d failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", i, r, tt.ok, tt.err, ok, err)
}
}
}
var andRuleTests = []struct {
desc string // description of the test case
rule AndRule
@ -552,8 +498,8 @@ var andRuleTests = []struct {
{
"(a=z, b=y): a=z && b=y",
AndRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false, nil}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false, nil}, ""}},
},
map[string]interface{}{"A": "z", "B": "y"}, nil, nil,
[]byte{},
@ -562,8 +508,8 @@ var andRuleTests = []struct {
{
"(a=z, b=Y): a=z && b=y",
AndRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false, nil}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false, nil}, ""}},
},
map[string]interface{}{"A": "z", "B": "Y"}, nil, nil,
[]byte{},
@ -573,22 +519,22 @@ var andRuleTests = []struct {
{
"(a=z, b=y, c=x, d=w=, e=X, f=X): a=z && (b=y && c=x) && (d=w || e=v) && !f=u",
AndRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false, nil}, ""}},
{
And: &AndRule{
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false, nil}, ""}},
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", "", false, nil}, ""}},
},
},
{
Or: &OrRule{
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", "", false, nil}, ""}},
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", "", false, nil}, ""}},
},
},
{
Not: &NotRule{
Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", "", false}, ""},
Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", "", false, nil}, ""},
},
},
},
@ -600,7 +546,7 @@ var andRuleTests = []struct {
// failures
{
"invalid rule",
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}},
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false, nil}, ""}}},
map[string]interface{}{"Y": "z"}, nil, nil, nil,
false, true,
},
@ -632,8 +578,8 @@ var orRuleTests = []struct {
{
"(a=z, b=X): a=z || b=y",
OrRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false, nil}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false, nil}, ""}},
},
map[string]interface{}{"A": "z", "B": "X"}, nil, nil,
[]byte{},
@ -642,8 +588,8 @@ var orRuleTests = []struct {
{
"(a=X, b=y): a=z || b=y",
OrRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false, nil}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false, nil}, ""}},
},
map[string]interface{}{"A": "X", "B": "y"}, nil, nil,
[]byte{},
@ -652,8 +598,8 @@ var orRuleTests = []struct {
{
"(a=Z, b=Y): a=z || b=y",
OrRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false, nil}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false, nil}, ""}},
},
map[string]interface{}{"A": "Z", "B": "Y"}, nil, nil,
[]byte{},
@ -663,7 +609,7 @@ var orRuleTests = []struct {
{
"missing parameter node",
OrRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false, nil}, ""}},
},
map[string]interface{}{"Y": "Z"}, nil, nil,
[]byte{},
@ -694,8 +640,8 @@ var notRuleTests = []struct {
ok bool
err bool
}{
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false},
{"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false},
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false, nil}, ""}}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false},
{"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false, nil}, ""}}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false},
}
func TestNotRule(t *testing.T) {

View file

@ -39,6 +39,7 @@ var (
hooksURLPrefix = flag.String("urlprefix", "hooks", "url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id)")
secure = flag.Bool("secure", false, "use HTTPS instead of HTTP")
asTemplate = flag.Bool("template", false, "parse hooks file as a Go template")
templateDelimiters = flag.String("template-delims", "", "a comma-separated pair of delimiters, e.g. '((,))' or '[[,]]' to use instead of the standard '{{,}}' when parsing hooks file as a template, to avoid clashing with any \"source\": \"template\" arguments")
cert = flag.String("cert", "cert.pem", "path to the HTTPS certificate pem file")
key = flag.String("key", "key.pem", "path to the HTTPS certificate private key pem file")
justDisplayVersion = flag.Bool("version", false, "display webhook version and quit")
@ -204,7 +205,7 @@ func main() {
newHooks := hook.Hooks{}
err := newHooks.LoadFromFile(hooksFilePath, *asTemplate)
err := newHooks.LoadFromFile(hooksFilePath, *asTemplate, *templateDelimiters)
if err != nil {
log.Printf("couldn't load hooks from file! %+v\n", err)
@ -670,7 +671,7 @@ func reloadHooks(hooksFilePath string) {
// parse and swap
log.Printf("attempting to reload hooks from %s\n", hooksFilePath)
err := hooksInFile.LoadFromFile(hooksFilePath, *asTemplate)
err := hooksInFile.LoadFromFile(hooksFilePath, *asTemplate, *templateDelimiters)
if err != nil {
log.Printf("couldn't load hooks from file! %+v\n", err)