Compare commits

..

21 Commits

Author SHA1 Message Date
Adnan Hajdarevic 8fe6c9a05d Update version to 2.6.11 2019-12-15 14:30:54 +01:00
Adnan Hajdarević 7c4e6e94fc Merge pull request #365 from moorereason/feature/364-constant-time
Use constant time string compare for match value
2019-12-11 07:06:12 +01:00
Cameron Moore 31e76bcd00 Use constant time string compare for match value
Fixes #364
2019-12-10 22:22:13 -06:00
Adnan Hajdarević c47c06e822 Merge pull request #363 from moorereason/feature/arm64-travisci
Add arm64 to Travis CI
2019-12-10 00:23:38 +01:00
Cameron Moore bf3d042da6 Use master instead of tip 2019-12-09 16:57:55 -06:00
Cameron Moore d05911cdcb Add arm64 to Travis CI 2019-12-09 16:50:19 -06:00
Adnan Hajdarević 634ca84807 Merge pull request #362 from moorereason/feature/cipher-suites
Feature/cipher suites
2019-12-09 21:52:25 +01:00
Cameron Moore 8c46a8343b Document minimum Go release 2019-12-05 17:01:38 -06:00
Cameron Moore 13d5630e80 Update docs for TLS version and cipher suite options 2019-12-03 21:36:14 -06:00
Cameron Moore f1003560f1 Add list cipher suites support 2019-12-03 21:35:16 -06:00
Cameron Moore 997db04b9f Require Go 1.12 or newer
Go 1.11 does not support TLS 1.3.  To simplify cipher suite selection,
we now require at least Go 1.12.
2019-12-03 21:31:23 -06:00
Cameron Moore 769e743563 Add missing files 2019-12-03 15:21:18 -06:00
Cameron Moore 43f519a712 Add TLS version and cipher suites options
Default to TLS 1.2 and secure cipher suites.

Built for Go 1.13. Code in cipher_suites.go taken from Go tip commit
0ee22d9, which is scheduled for the upcoming Go 1.14 release.  Once Go
1.14 is released, we can remove this file and use the stdlib.

Fixes #244
2019-12-03 15:13:12 -06:00
Adnan Hajdarević a617b1a6ac Merge pull request #361 from adnanh/feature/check-payload-hash-sha512
Add SHA512 payload check rule
2019-12-02 22:34:07 +01:00
Adnan Hajdarević 9117f4f6d6 Merge pull request #360 from adnanh/improvement/content-type-based-payload-parsing
Fix invalid assumption that multipart forms can be parsed in te same way as urlencoded forms.
2019-12-02 22:33:45 +01:00
Adnan Hajdarevic b53996f175 Add tests for SHA512 payload hash check rule. 2019-12-02 19:49:56 +01:00
Adnan Hajdarevic 154177e46a Add documentation for SHA512 payload hash check rule. 2019-12-02 19:49:34 +01:00
Adnan Hajdarevic d4e98281d7 Add SHA512 payload check rule. 2019-12-02 19:48:59 +01:00
Adnan Hajdarevic ce186487f4 Format the file using go fmt. 2019-12-02 19:03:38 +01:00
Adnan Hajdarevic 1110f82443 Add test for unsupported content type error message. 2019-12-02 19:01:20 +01:00
Adnan Hajdarevic a99abd4e6f Fix invalid assumption in code that multipart forms can be parsed in the same way as urlencoded forms.
Refactored code to use switch-case statement over the `Content-Type` header and log unsupported content types instead of silently failing.
Also made the `x-www-form-urlencoded` content type handler more specific (as opposed to the previous code which looked for `form` occurence in the value),
as we need to use different logic for multipart forms, which we'll hopefully implement soon.

The issue with multipart forms that we have to handle first is that the files are being written to temporary files, and as such, for async hooks
webhook cannot guarantee they'll be available after we close the request; that, and the fact that we don't have code that will properly serialize
and pass such Golang objects to the script, as there are several fields which might be interesting to the end user.
2019-12-02 18:49:24 +01:00
10 changed files with 354 additions and 22 deletions
+11 -4
View File
@@ -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 ./...
+3 -1
View File
@@ -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.
+102
View File
@@ -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
View File
@@ -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.
+7 -1
View File
@@ -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
@@ -35,4 +41,4 @@ Use any of the above specified flags to override their default behavior.
If you are running an OS that supports USR1 signal, you can use it to trigger hooks reload from hooks file, without restarting the webhook instance. If you are running an OS that supports USR1 signal, you can use it to trigger hooks reload from hooks file, without restarting the webhook instance.
```bash ```bash
kill -USR1 webhookpid kill -USR1 webhookpid
``` ```
+33 -1
View File
@@ -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)
+41
View File
@@ -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)
}
}
}
+85
View File
@@ -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
}
+37 -13
View File
@@ -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
@@ -338,8 +362,8 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in
// check the command exists // check the command exists
cmdPath, err := exec.LookPath(h.ExecuteCommand) cmdPath, err := exec.LookPath(h.ExecuteCommand)
if err != nil { if err != nil {
// give a last chance, maybe is a relative path // give a last chance, maybe is a relative path
relativeToCwd := filepath.Join(h.CommandWorkingDirectory, h.ExecuteCommand) relativeToCwd := filepath.Join(h.CommandWorkingDirectory, h.ExecuteCommand)
// check the command exists // check the command exists
cmdPath, err = exec.LookPath(relativeToCwd) cmdPath, err = exec.LookPath(relativeToCwd)
} }
+1
View File
@@ -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.