mirror of
https://github.com/adnanh/webhook.git
synced 2026-07-27 01:29:16 +08:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fe6c9a05d | |||
| 7c4e6e94fc | |||
| 31e76bcd00 | |||
| c47c06e822 | |||
| bf3d042da6 | |||
| d05911cdcb | |||
| 634ca84807 | |||
| 8c46a8343b | |||
| 13d5630e80 | |||
| f1003560f1 | |||
| 997db04b9f | |||
| 769e743563 | |||
| 43f519a712 | |||
| a617b1a6ac | |||
| 9117f4f6d6 | |||
| b53996f175 | |||
| 154177e46a | |||
| d4e98281d7 | |||
| ce186487f4 | |||
| 1110f82443 | |||
| a99abd4e6f |
+11
-4
@@ -1,23 +1,30 @@
|
|||||||
language: go
|
language: go
|
||||||
|
|
||||||
go:
|
go:
|
||||||
- 1.11.x
|
|
||||||
- 1.12.x
|
- 1.12.x
|
||||||
- 1.13.x
|
- 1.13.x
|
||||||
- tip
|
- master
|
||||||
|
|
||||||
os:
|
os:
|
||||||
- linux
|
- linux
|
||||||
- osx
|
- osx
|
||||||
- windows
|
- windows
|
||||||
|
|
||||||
|
arch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- go: tip
|
- go: master
|
||||||
exclude:
|
exclude:
|
||||||
- os: windows
|
- os: windows
|
||||||
go: tip
|
go: master
|
||||||
|
- os: windows
|
||||||
|
arch: arm64
|
||||||
|
- os: osx
|
||||||
|
arch: arm64
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- go get -d -v -t ./...
|
- go get -d -v -t ./...
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ If you don't have time to waste configuring, hosting, debugging and maintaining
|
|||||||
# Getting started
|
# Getting started
|
||||||
## Installation
|
## Installation
|
||||||
### Building from source
|
### Building from source
|
||||||
To get started, first make sure you've properly set up your [Golang](http://golang.org/doc/install) environment and then run the
|
To get started, first make sure you've properly set up your [Go](http://golang.org/doc/install) 1.12 or newer environment and then run
|
||||||
```bash
|
```bash
|
||||||
$ go get github.com/adnanh/webhook
|
$ go get github.com/adnanh/webhook
|
||||||
```
|
```
|
||||||
@@ -83,6 +83,8 @@ However, hook defined like that could pose a security threat to your system, bec
|
|||||||
## Using HTTPS
|
## Using HTTPS
|
||||||
[webhook][w] by default serves hooks using http. If you want [webhook][w] to serve secure content using https, you can use the `-secure` flag while starting [webhook][w]. Files containing a certificate and matching private key for the server must be provided using the `-cert /path/to/cert.pem` and `-key /path/to/key.pem` flags. If the certificate is signed by a certificate authority, the cert file should be the concatenation of the server's certificate followed by the CA's certificate.
|
[webhook][w] by default serves hooks using http. If you want [webhook][w] to serve secure content using https, you can use the `-secure` flag while starting [webhook][w]. Files containing a certificate and matching private key for the server must be provided using the `-cert /path/to/cert.pem` and `-key /path/to/key.pem` flags. If the certificate is signed by a certificate authority, the cert file should be the concatenation of the server's certificate followed by the CA's certificate.
|
||||||
|
|
||||||
|
TLS version and cipher suite selection flags are available from the command line. To list available cipher suites, use the `-list-cipher-suites` flag. The `-tls-min-version` flag can be used with `-list-cipher-suites`.
|
||||||
|
|
||||||
## CORS Headers
|
## CORS Headers
|
||||||
If you want to set CORS headers, you can use the `-header name=value` flag while starting [webhook][w] to set the appropriate CORS headers that will be returned with each response.
|
If you want to set CORS headers, you can use the `-header name=value` flag while starting [webhook][w] to set the appropriate CORS headers that will be returned with each response.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
// Copyright 2010 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Copied from Go 1.14 tip src/crypto/tls/cipher_suites.go
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CipherSuite is a TLS cipher suite. Note that most functions in this package
|
||||||
|
// accept and expose cipher suite IDs instead of this type.
|
||||||
|
type CipherSuite struct {
|
||||||
|
ID uint16
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// Supported versions is the list of TLS protocol versions that can
|
||||||
|
// negotiate this cipher suite.
|
||||||
|
SupportedVersions []uint16
|
||||||
|
|
||||||
|
// Insecure is true if the cipher suite has known security issues
|
||||||
|
// due to its primitives, design, or implementation.
|
||||||
|
Insecure bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
supportedUpToTLS12 = []uint16{tls.VersionTLS10, tls.VersionTLS11, tls.VersionTLS12}
|
||||||
|
supportedOnlyTLS12 = []uint16{tls.VersionTLS12}
|
||||||
|
supportedOnlyTLS13 = []uint16{tls.VersionTLS13}
|
||||||
|
)
|
||||||
|
|
||||||
|
// CipherSuites returns a list of cipher suites currently implemented by this
|
||||||
|
// package, excluding those with security issues, which are returned by
|
||||||
|
// InsecureCipherSuites.
|
||||||
|
//
|
||||||
|
// The list is sorted by ID. Note that the default cipher suites selected by
|
||||||
|
// this package might depend on logic that can't be captured by a static list.
|
||||||
|
func CipherSuites() []*CipherSuite {
|
||||||
|
return []*CipherSuite{
|
||||||
|
{tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, "TLS_RSA_WITH_3DES_EDE_CBC_SHA", supportedUpToTLS12, false},
|
||||||
|
{tls.TLS_RSA_WITH_AES_128_CBC_SHA, "TLS_RSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
|
||||||
|
{tls.TLS_RSA_WITH_AES_256_CBC_SHA, "TLS_RSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
|
||||||
|
{tls.TLS_RSA_WITH_AES_128_GCM_SHA256, "TLS_RSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
|
||||||
|
{tls.TLS_RSA_WITH_AES_256_GCM_SHA384, "TLS_RSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},
|
||||||
|
|
||||||
|
{tls.TLS_AES_128_GCM_SHA256, "TLS_AES_128_GCM_SHA256", supportedOnlyTLS13, false},
|
||||||
|
{tls.TLS_AES_256_GCM_SHA384, "TLS_AES_256_GCM_SHA384", supportedOnlyTLS13, false},
|
||||||
|
{tls.TLS_CHACHA20_POLY1305_SHA256, "TLS_CHACHA20_POLY1305_SHA256", supportedOnlyTLS13, false},
|
||||||
|
|
||||||
|
{tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
|
||||||
|
{tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
|
||||||
|
{tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", supportedUpToTLS12, false},
|
||||||
|
{tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
|
||||||
|
{tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
|
||||||
|
{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
|
||||||
|
{tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},
|
||||||
|
{tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
|
||||||
|
{tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},
|
||||||
|
|
||||||
|
// go1.14
|
||||||
|
// {tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", supportedOnlyTLS12, false},
|
||||||
|
// {tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", supportedOnlyTLS12, false},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsecureCipherSuites returns a list of cipher suites currently implemented by
|
||||||
|
// this package and which have security issues.
|
||||||
|
//
|
||||||
|
// Most applications should not use the cipher suites in this list, and should
|
||||||
|
// only use those returned by CipherSuites.
|
||||||
|
func InsecureCipherSuites() []*CipherSuite {
|
||||||
|
// RC4 suites are broken because RC4 is.
|
||||||
|
// CBC-SHA256 suites have no Lucky13 countermeasures.
|
||||||
|
return []*CipherSuite{
|
||||||
|
{tls.TLS_RSA_WITH_RC4_128_SHA, "TLS_RSA_WITH_RC4_128_SHA", supportedUpToTLS12, true},
|
||||||
|
{tls.TLS_RSA_WITH_AES_128_CBC_SHA256, "TLS_RSA_WITH_AES_128_CBC_SHA256", supportedOnlyTLS12, true},
|
||||||
|
{tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", supportedUpToTLS12, true},
|
||||||
|
{tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, "TLS_ECDHE_RSA_WITH_RC4_128_SHA", supportedUpToTLS12, true},
|
||||||
|
{tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", supportedOnlyTLS12, true},
|
||||||
|
{tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", supportedOnlyTLS12, true},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CipherSuiteName returns the standard name for the passed cipher suite ID
|
||||||
|
// (e.g. "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"), or a fallback representation
|
||||||
|
// of the ID value if the cipher suite is not implemented by this package.
|
||||||
|
func CipherSuiteName(id uint16) string {
|
||||||
|
for _, c := range CipherSuites() {
|
||||||
|
if c.ID == id {
|
||||||
|
return c.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, c := range InsecureCipherSuites() {
|
||||||
|
if c.ID == id {
|
||||||
|
return c.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("0x%04X", id)
|
||||||
|
}
|
||||||
+34
-2
@@ -186,7 +186,39 @@ For the regex syntax, check out <http://golang.org/pkg/regexp/syntax/>
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Match Whitelisted IP range
|
### 4. Match payload-hash-sha256
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"match":
|
||||||
|
{
|
||||||
|
"type": "payload-hash-sha256",
|
||||||
|
"secret": "yoursecret",
|
||||||
|
"parameter":
|
||||||
|
{
|
||||||
|
"source": "header",
|
||||||
|
"name": "X-Signature"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Match payload-hash-sha512
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"match":
|
||||||
|
{
|
||||||
|
"type": "payload-hash-sha512",
|
||||||
|
"secret": "yoursecret",
|
||||||
|
"parameter":
|
||||||
|
{
|
||||||
|
"source": "header",
|
||||||
|
"name": "X-Signature"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 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`.
|
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`.
|
||||||
|
|
||||||
@@ -200,7 +232,7 @@ The IP can be IPv4- or IPv6-formatted, using [CIDR notation](https://en.wikipedi
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. Match scalr-signature
|
### 7. Match scalr-signature
|
||||||
|
|
||||||
The trigger rule checks the scalr signature and also checks that the request was signed less than 5 minutes before it was received.
|
The trigger rule checks the scalr signature and also checks that the request was signed less than 5 minutes before it was received.
|
||||||
A unqiue signing key is generated for each webhook endpoint URL you register in Scalr.
|
A unqiue signing key is generated for each webhook endpoint URL you register in Scalr.
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
Usage of webhook:
|
Usage of webhook:
|
||||||
-cert string
|
-cert string
|
||||||
path to the HTTPS certificate pem file (default "cert.pem")
|
path to the HTTPS certificate pem file (default "cert.pem")
|
||||||
|
-cipher-suites string
|
||||||
|
comma-separated list of supported TLS cipher suites
|
||||||
-header value
|
-header value
|
||||||
response header to return, specified in format name=value, use multiple times to set multiple headers
|
response header to return, specified in format name=value, use multiple times to set multiple headers
|
||||||
-hooks value
|
-hooks value
|
||||||
@@ -13,6 +15,8 @@ Usage of webhook:
|
|||||||
ip the webhook should serve hooks on (default "0.0.0.0")
|
ip the webhook should serve hooks on (default "0.0.0.0")
|
||||||
-key string
|
-key string
|
||||||
path to the HTTPS certificate private key pem file (default "key.pem")
|
path to the HTTPS certificate private key pem file (default "key.pem")
|
||||||
|
-list-cipher-suites
|
||||||
|
list available TLS cipher suites
|
||||||
-nopanic
|
-nopanic
|
||||||
do not panic if hooks cannot be loaded when webhook is not running in verbose mode
|
do not panic if hooks cannot be loaded when webhook is not running in verbose mode
|
||||||
-port int
|
-port int
|
||||||
@@ -21,6 +25,8 @@ Usage of webhook:
|
|||||||
use HTTPS instead of HTTP
|
use HTTPS instead of HTTP
|
||||||
-template
|
-template
|
||||||
parse hooks file as a Go template
|
parse hooks file as a Go template
|
||||||
|
-tls-min-version string
|
||||||
|
minimum TLS version (1.0, 1.1, 1.2, 1.3) (default "1.2")
|
||||||
-urlprefix string
|
-urlprefix string
|
||||||
url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id) (default "hooks")
|
url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id) (default "hooks")
|
||||||
-verbose
|
-verbose
|
||||||
|
|||||||
+33
-1
@@ -5,6 +5,8 @@ import (
|
|||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"crypto/subtle"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -134,6 +136,27 @@ func CheckPayloadSignature256(payload []byte, secret string, signature string) (
|
|||||||
return expectedMAC, err
|
return expectedMAC, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckPayloadSignature512 calculates and verifies SHA512 signature of the given payload
|
||||||
|
func CheckPayloadSignature512(payload []byte, secret string, signature string) (string, error) {
|
||||||
|
if secret == "" {
|
||||||
|
return "", errors.New("signature validation secret can not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
signature = strings.TrimPrefix(signature, "sha512=")
|
||||||
|
|
||||||
|
mac := hmac.New(sha512.New, []byte(secret))
|
||||||
|
_, err := mac.Write(payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
||||||
|
|
||||||
|
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
|
||||||
|
return expectedMAC, &SignatureError{signature}
|
||||||
|
}
|
||||||
|
return expectedMAC, err
|
||||||
|
}
|
||||||
|
|
||||||
func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey string, checkDate bool) (bool, error) {
|
func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey string, checkDate bool) (bool, error) {
|
||||||
// Check for the signature and date headers
|
// Check for the signature and date headers
|
||||||
if _, ok := headers["X-Signature"]; !ok {
|
if _, ok := headers["X-Signature"]; !ok {
|
||||||
@@ -748,6 +771,7 @@ const (
|
|||||||
MatchRegex string = "regex"
|
MatchRegex string = "regex"
|
||||||
MatchHashSHA1 string = "payload-hash-sha1"
|
MatchHashSHA1 string = "payload-hash-sha1"
|
||||||
MatchHashSHA256 string = "payload-hash-sha256"
|
MatchHashSHA256 string = "payload-hash-sha256"
|
||||||
|
MatchHashSHA512 string = "payload-hash-sha512"
|
||||||
IPWhitelist string = "ip-whitelist"
|
IPWhitelist string = "ip-whitelist"
|
||||||
ScalrSignature string = "scalr-signature"
|
ScalrSignature string = "scalr-signature"
|
||||||
)
|
)
|
||||||
@@ -764,7 +788,7 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod
|
|||||||
if arg, ok := r.Parameter.Get(headers, query, payload); ok {
|
if arg, ok := r.Parameter.Get(headers, query, payload); ok {
|
||||||
switch r.Type {
|
switch r.Type {
|
||||||
case MatchValue:
|
case MatchValue:
|
||||||
return arg == r.Value, nil
|
return compare(arg, r.Value), nil
|
||||||
case MatchRegex:
|
case MatchRegex:
|
||||||
return regexp.MatchString(r.Regex, arg)
|
return regexp.MatchString(r.Regex, arg)
|
||||||
case MatchHashSHA1:
|
case MatchHashSHA1:
|
||||||
@@ -773,11 +797,19 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod
|
|||||||
case MatchHashSHA256:
|
case MatchHashSHA256:
|
||||||
_, err := CheckPayloadSignature256(*body, r.Secret, arg)
|
_, err := CheckPayloadSignature256(*body, r.Secret, arg)
|
||||||
return err == nil, err
|
return err == nil, err
|
||||||
|
case MatchHashSHA512:
|
||||||
|
_, err := CheckPayloadSignature512(*body, r.Secret, arg)
|
||||||
|
return err == nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compare is a helper function for constant time string comparisons.
|
||||||
|
func compare(a, b string) bool {
|
||||||
|
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
||||||
|
}
|
||||||
|
|
||||||
// getenv provides a template function to retrieve OS environment variables.
|
// getenv provides a template function to retrieve OS environment variables.
|
||||||
func getenv(s string) string {
|
func getenv(s string) string {
|
||||||
return os.Getenv(s)
|
return os.Getenv(s)
|
||||||
|
|||||||
@@ -94,6 +94,33 @@ func TestCheckPayloadSignature256(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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},
|
||||||
|
// failures
|
||||||
|
{[]byte(`{"a": "z"}`), "secret", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", false},
|
||||||
|
{[]byte(`{"a": "z"}`), "", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "", 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 {
|
var checkScalrSignatureTests = []struct {
|
||||||
description string
|
description string
|
||||||
headers map[string]interface{}
|
headers map[string]interface{}
|
||||||
@@ -601,3 +628,17 @@ func TestNotRule(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCompare(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
a, b string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{"abcd", "abcd", true},
|
||||||
|
{"zyxw", "abcd", false},
|
||||||
|
} {
|
||||||
|
if ok := compare(tt.a, tt.b); ok != tt.ok {
|
||||||
|
t.Errorf("compare failed for %q and %q: got %v\n", tt.a, tt.b, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeTLSSupportedCipherStrings(w io.Writer, min uint16) error {
|
||||||
|
for _, c := range CipherSuites() {
|
||||||
|
var found bool
|
||||||
|
|
||||||
|
for _, v := range c.SupportedVersions {
|
||||||
|
if v >= min {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := w.Write([]byte(c.Name + "\n"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTLSMinVersion converts a version string into a TLS version ID.
|
||||||
|
func getTLSMinVersion(v string) uint16 {
|
||||||
|
switch v {
|
||||||
|
case "1.0":
|
||||||
|
return tls.VersionTLS10
|
||||||
|
case "1.1":
|
||||||
|
return tls.VersionTLS11
|
||||||
|
case "1.2", "":
|
||||||
|
return tls.VersionTLS12
|
||||||
|
case "1.3":
|
||||||
|
return tls.VersionTLS13
|
||||||
|
default:
|
||||||
|
log.Fatalln("error: unknown minimum TLS version:", v)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTLSCipherSuites converts a comma separated list of cipher suites into a
|
||||||
|
// slice of TLS cipher suite IDs.
|
||||||
|
func getTLSCipherSuites(v string) []uint16 {
|
||||||
|
supported := CipherSuites()
|
||||||
|
|
||||||
|
if v == "" {
|
||||||
|
suites := make([]uint16, len(supported))
|
||||||
|
|
||||||
|
for _, cs := range supported {
|
||||||
|
suites = append(suites, cs.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return suites
|
||||||
|
}
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
txts := strings.Split(v, ",")
|
||||||
|
suites := make([]uint16, len(txts))
|
||||||
|
|
||||||
|
for _, want := range txts {
|
||||||
|
found = false
|
||||||
|
|
||||||
|
for _, cs := range supported {
|
||||||
|
if want == cs.Name {
|
||||||
|
suites = append(suites, cs.ID)
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
log.Fatalln("error: unknown TLS cipher suite:", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return suites
|
||||||
|
}
|
||||||
+35
-11
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -18,13 +19,13 @@ import (
|
|||||||
|
|
||||||
"github.com/codegangsta/negroni"
|
"github.com/codegangsta/negroni"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/satori/go.uuid"
|
uuid "github.com/satori/go.uuid"
|
||||||
|
|
||||||
fsnotify "gopkg.in/fsnotify.v1"
|
fsnotify "gopkg.in/fsnotify.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
version = "2.6.10"
|
version = "2.6.11"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -39,6 +40,9 @@ var (
|
|||||||
cert = flag.String("cert", "cert.pem", "path to the HTTPS certificate pem file")
|
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")
|
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")
|
justDisplayVersion = flag.Bool("version", false, "display webhook version and quit")
|
||||||
|
justListCiphers = flag.Bool("list-cipher-suites", false, "list available TLS cipher suites")
|
||||||
|
tlsMinVersion = flag.String("tls-min-version", "1.2", "minimum TLS version (1.0, 1.1, 1.2, 1.3)")
|
||||||
|
tlsCipherSuites = flag.String("cipher-suites", "", "comma-separated list of supported TLS cipher suites")
|
||||||
|
|
||||||
responseHeaders hook.ResponseHeaders
|
responseHeaders hook.ResponseHeaders
|
||||||
hooksFiles hook.HooksFiles
|
hooksFiles hook.HooksFiles
|
||||||
@@ -79,6 +83,14 @@ func main() {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *justListCiphers {
|
||||||
|
err := writeTLSSupportedCipherStrings(os.Stdout, getTLSMinVersion(*tlsMinVersion))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
if len(hooksFiles) == 0 {
|
if len(hooksFiles) == 0 {
|
||||||
hooksFiles = append(hooksFiles, "hooks.json")
|
hooksFiles = append(hooksFiles, "hooks.json")
|
||||||
}
|
}
|
||||||
@@ -194,18 +206,28 @@ func main() {
|
|||||||
|
|
||||||
n.UseHandler(router)
|
n.UseHandler(router)
|
||||||
|
|
||||||
if *secure {
|
if !*secure {
|
||||||
log.Printf("serving hooks on https://%s:%d%s", *ip, *port, hooksURL)
|
|
||||||
log.Fatal(http.ListenAndServeTLS(fmt.Sprintf("%s:%d", *ip, *port), *cert, *key, n))
|
|
||||||
} else {
|
|
||||||
log.Printf("serving hooks on http://%s:%d%s", *ip, *port, hooksURL)
|
log.Printf("serving hooks on http://%s:%d%s", *ip, *port, hooksURL)
|
||||||
log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", *ip, *port), n))
|
log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", *ip, *port), n))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svr := &http.Server{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", *ip, *port),
|
||||||
|
Handler: n,
|
||||||
|
TLSConfig: &tls.Config{
|
||||||
|
CipherSuites: getTLSCipherSuites(*tlsCipherSuites),
|
||||||
|
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
|
||||||
|
MinVersion: getTLSMinVersion(*tlsMinVersion),
|
||||||
|
PreferServerCipherSuites: true,
|
||||||
|
},
|
||||||
|
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0), // disable http/2
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("serving hooks on https://%s:%d%s", *ip, *port, hooksURL)
|
||||||
|
log.Fatal(svr.ListenAndServeTLS(*cert, *key))
|
||||||
}
|
}
|
||||||
|
|
||||||
func hookHandler(w http.ResponseWriter, r *http.Request) {
|
func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// generate a request id for logging
|
// generate a request id for logging
|
||||||
rid := uuid.NewV4().String()[:6]
|
rid := uuid.NewV4().String()[:6]
|
||||||
|
|
||||||
@@ -240,22 +262,24 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
contentType = matchedHook.IncomingPayloadContentType
|
contentType = matchedHook.IncomingPayloadContentType
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(contentType, "json") {
|
switch {
|
||||||
|
case strings.Contains(contentType, "json"):
|
||||||
decoder := json.NewDecoder(strings.NewReader(string(body)))
|
decoder := json.NewDecoder(strings.NewReader(string(body)))
|
||||||
decoder.UseNumber()
|
decoder.UseNumber()
|
||||||
|
|
||||||
err := decoder.Decode(&payload)
|
err := decoder.Decode(&payload)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[%s] error parsing JSON payload %+v\n", rid, err)
|
log.Printf("[%s] error parsing JSON payload %+v\n", rid, err)
|
||||||
}
|
}
|
||||||
} else if strings.Contains(contentType, "form") {
|
case strings.Contains(contentType, "x-www-form-urlencoded"):
|
||||||
fd, err := url.ParseQuery(string(body))
|
fd, err := url.ParseQuery(string(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[%s] error parsing form payload %+v\n", rid, err)
|
log.Printf("[%s] error parsing form payload %+v\n", rid, err)
|
||||||
} else {
|
} else {
|
||||||
payload = valuesToMap(fd)
|
payload = valuesToMap(fd)
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
log.Printf("[%s] error parsing body payload due to unsupported content type header: %s\n", rid, contentType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle hook
|
// handle hook
|
||||||
@@ -272,7 +296,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
ok, err = matchedHook.TriggerRule.Evaluate(&headers, &query, &payload, &body, r.RemoteAddr)
|
ok, err = matchedHook.TriggerRule.Evaluate(&headers, &query, &payload, &body, r.RemoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := fmt.Sprintf("[%s] error evaluating hook: %s", rid, err)
|
msg := fmt.Sprintf("[%s] error evaluating hook: %s", rid, err)
|
||||||
log.Print(msg)
|
log.Println(msg)
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
fmt.Fprint(w, "Error occurred while evaluating hook rules.")
|
fmt.Fprint(w, "Error occurred while evaluating hook rules.")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -617,6 +617,7 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
|
|||||||
// Check logs
|
// Check logs
|
||||||
{"static params should pass", "static-params-ok", nil, `{}`, false, http.StatusOK, "arg: passed\n", `(?s)command output: arg: passed`},
|
{"static params should pass", "static-params-ok", nil, `{}`, false, http.StatusOK, "arg: passed\n", `(?s)command output: arg: passed`},
|
||||||
{"command with space logs warning", "warn-on-space", nil, `{}`, false, http.StatusInternalServerError, "Error occurred while executing the hook's command. Please check your logs for more details.", `(?s)unable to locate command.*use 'pass[-]arguments[-]to[-]command' to specify args`},
|
{"command with space logs warning", "warn-on-space", nil, `{}`, false, http.StatusInternalServerError, "Error occurred while executing the hook's command. Please check your logs for more details.", `(?s)unable to locate command.*use 'pass[-]arguments[-]to[-]command' to specify args`},
|
||||||
|
{"unsupported content type error", "github", map[string]string{"Content-Type": "nonexistent/format"}, `{}`, false, http.StatusBadRequest, `Hook rules were not satisfied.`, `(?s)error parsing body payload due to unsupported content type header:`},
|
||||||
}
|
}
|
||||||
|
|
||||||
// buffer provides a concurrency-safe bytes.Buffer to tests above.
|
// buffer provides a concurrency-safe bytes.Buffer to tests above.
|
||||||
|
|||||||
Reference in New Issue
Block a user