Compare commits

..

11 Commits

11 changed files with 163 additions and 54 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
gohookr

View File

@@ -3,15 +3,17 @@ all: install
clean:
rm -rf gohookr
install:
go mod tidy
go build -o gohookr
install: build
cp gohookr /usr/local/bin/
cp gohookr.service /usr/lib/systemd/system/
cp -n config.json /etc/gohookr.json
systemctl daemon-reload
systemctl enable --now gohookr
build:
go mod tidy
go build -o gohookr
uninstall:
systemctl disable --now gohookr
rm -rf /usr/local/bin/gohookr /usr/lib/systemd/system/gohookr.service

View File

@@ -6,15 +6,28 @@
"Program": "./example.sh",
"AppendPayload": true
},
"Secret": "THISISVERYSECRET",
"SignatureHeader": "X-Hub-Signature",
"SignaturePrefix": "sha256=",
"DisableSignatureVerification": true,
"Tests": [
{
"Program": "echo",
"Arguments": [ "test" ]
}
]
},
"fails_tests": {
"Script": {
"Program": "./example.sh",
"AppendPayload": true
},
"Secret": "who_cares",
"SignatureHeader": "X-Hub-Signature-256",
"SignaturePrefix": "sha256=",
"Tests": [
{
"Program": "false",
"Arguments": []
}
]
}
}
}

13
config.yml Normal file
View File

@@ -0,0 +1,13 @@
listenaddress: 127.0.0.1:8654
services:
test:
script:
program: "echo"
arguments:
- test
tests:
- program: ./example.sh
appendpayload: true
disablesignatureverification: false
signatureheader: test
secret: thisisasecret

View File

@@ -1,6 +1,9 @@
package config
import "os/exec"
import (
"fmt"
"os/exec"
)
type Command struct {
Program string
@@ -17,3 +20,11 @@ func (c Command) Execute(payload string) ([]byte, error) {
return exec.Command(c.Program, arguments...).Output()
}
func (c Command) String() string {
return fmt.Sprintf(
"<Command cmd=%v AppendPayload=%v>",
append([]string{c.Program}, c.Arguments...),
c.AppendPayload,
)
}

View File

@@ -2,9 +2,12 @@ package config
import (
"encoding/json"
"fmt"
"io/ioutil"
"gopkg.in/yaml.v3"
)
// The struct that represents the config.json file
type Config struct {
ListenAddress string
Services map[string]struct {
@@ -12,29 +15,48 @@ type Config struct {
Secret string
SignaturePrefix string
SignatureHeader string
DisableSignatureVerification bool
Tests []Command
}
}
// Check that all required fields are filled in
func (c Config) Validate() error {
if c.ListenAddress == "" {
return requiredFieldError{"ListenAddress", ""}
}
jsonbytes, _ := json.MarshalIndent(c, "", " ")
fmt.Println(string(jsonbytes))
for serviceName, service := range c.Services {
if service.Script.Program == "" {
return requiredFieldError{"Script.Program", serviceName}
}
if service.SignatureHeader == "" {
if !service.DisableSignatureVerification && service.SignatureHeader == "" {
return requiredFieldError{"SignatureHeader", serviceName}
}
if service.Secret == "" {
if !service.DisableSignatureVerification && service.Secret == "" {
return requiredFieldError{"Secret", serviceName}
}
}
return nil
}
func (c *Config) Load(config_filename string) error {
raw_config, err := ioutil.ReadFile(config_filename)
if err != nil {
return err
}
err = json.Unmarshal(raw_config, &c)
if err == nil {
return c.Validate()
}
err = yaml.Unmarshal(raw_config, &c)
if err == nil {
return c.Validate()
}
return err
}

0
example.sh Executable file → Normal file
View File

7
go.mod
View File

@@ -1,5 +1,8 @@
module git.alra.uk/alvierahman90/gohookr
module git.alv.cx/alvierahman90/gohookr
go 1.16
require github.com/gorilla/mux v1.8.0
require (
github.com/gorilla/mux v1.8.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)

4
go.sum
View File

@@ -1,2 +1,6 @@
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

52
main.go
View File

@@ -4,7 +4,6 @@ import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/ioutil"
@@ -12,15 +11,16 @@ import (
"net/http"
"os"
"git.alra.uk/alvierahman90/gohookr/config"
"git.alv.cx/alvierahman90/gohookr/config"
"github.com/gorilla/mux"
)
var config_filename = "/etc/gohookr.json"
var checkSignature = true
var c config.Config
func main() {
var c config.Config
r := mux.NewRouter()
r.HandleFunc("/webhooks/{service}", webhookHandler)
@@ -32,35 +32,48 @@ func main() {
checkSignature = p != "true"
}
raw_config, err := ioutil.ReadFile(config_filename)
var err = c.Load(config_filename)
if err != nil {
panic(err.Error())
}
fmt.Printf("CONFIG OK: %s\n", config_filename)
fmt.Printf("LISTENING AT: %s\n", c.ListenAddress)
json.Unmarshal(raw_config, &c)
if err := c.Validate(); err != nil {
panic(err.Error())
for _, v := range os.Args {
if v == "checkConfig" {
return
}
}
log.Fatal(http.ListenAndServe(c.ListenAddress, r))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
var c config.Config
var err = c.Load(config_filename)
if err != nil {
writeResponse(w, 500, "Unable to read config file")
}
// Check what service is specified in URL (/webhooks/{service}) and if it exists
serviceName := string(mux.Vars(r)["service"])
service, ok := c.Services[serviceName]
if !ok {
writeResponse(w, 404, "Service Not Found")
fmt.Printf("Service not found: %v\n", serviceName)
return
}
fmt.Printf("Got webhook for: %v\n", serviceName)
// Read payload or return 500 if that doesn't work out
payload := ""
if p, err := ioutil.ReadAll(r.Body); err != nil {
writeResponse(w, 500, "Internal Server Error: Could not read payload")
fmt.Println("Error: Could not read payload")
return
} else {
payload = string(p)
}
// check what service is specified in URL (/webhooks/{service}) and if it exists
service, ok := c.Services[string(mux.Vars(r)["service"])]
if !ok {
writeResponse(w, 404, "Service Not Found")
return
}
// Verify that signature provided matches signature calculated using secretsss
signature := r.Header.Get(service.SignatureHeader)
calculatedSignature := fmt.Sprintf(
@@ -70,19 +83,18 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
)
fmt.Printf("signature = %v\n", signature)
fmt.Printf("calcuatedSignature = %v\n", calculatedSignature)
if signature != calculatedSignature && checkSignature {
if checkSignature && !service.DisableSignatureVerification && signature != calculatedSignature {
writeResponse(w, 400, "Bad Request: Signatures do not match")
fmt.Println("Signatures do not match!")
return
}
// run test and script in parralel to prevent timing out
go func(){
// Run tests and script as goroutine to prevent timing out
go func() {
// Run tests, immediately stop if one fails
for _, test := range service.Tests {
if _, err := test.Execute(payload); err != nil {
writeResponse(w, 409,
fmt.Sprintf("Conflict: Test failed: %v", err.Error()),
)
fmt.Printf("Test failed(%v) for service %v\n", test, serviceName)
return
}
}

View File

@@ -2,11 +2,6 @@
A _really_ simple webhook receiver, which listens at `/webhooks/<webhook-name>`.
Default config path is `/etc/gohookr.conf` and can be overriden by setting environment variable
`CONFIG`.
Check below for an example configuration.
## Installation
After you [install go](https://golang.org/doc/install):
@@ -15,7 +10,24 @@ After you [install go](https://golang.org/doc/install):
make
```
## Signature Verification
## Configuration
Default config path is `/etc/gohookr.json`.
It can be overriden by setting environment variable `CONFIG`.
The config file will be re-read every request so service configs can be changed without restarting
the service (unless you want to change the listening port).
Check below for an example configuration, which should tell you most of the things you need to know
to configure gohookr.
You can test your config file by running
```
gohookr checkConfig
```
### Signature Verification
Signature verificaiton is done using SHA256 HMACs.
You **must** set which HTTP header gohookr will receive a signature from using the `SignatureHeader`
@@ -25,18 +37,21 @@ You should also specify a shared secret in the `Secret` key.
You may also need to specify a `SignaturePrefix`.
For GitHub it would be `sha256=`.
### Disable Signature Verification
#### Disable Signature Verification
You can disable signature verification altogether by setting environment variable
You can disable signature verification by setting `DisableSignatureVerification` for a service to
`true`.
You can disable signature verification for all services by setting environment variable
`NO_SIGNATURE_VERIFICATION` to `true`.
## Writing Commands
### Writing Commands
gohookr doesn't care what the command is as long as the `Program` is executable.
You can specify extra arguments with the `Arguments` field.
You can ask it to put the payload as the last argument by setting `AppendPayload` to true.
## Writing Tests
### Writing Tests
gohookr can run test before running your script.
Tests must be in the form of bash scripts.
@@ -46,7 +61,7 @@ deploy.
Tests are run in the order they're listed so any actions that need to be done before
tests are run can simply be put in this section before the tests.
## Example Config
### Example Config
Required config keys are `ListenAddress` and `Services`.
@@ -63,15 +78,28 @@ An example config file can be found [here](./config.json) but also below:
"Program": "./example.sh",
"AppendPayload": true
},
"Secret": "THISISVERYSECRET",
"SignatureHeader": "X-Hub-Signature",
"SignaturePrefix": "sha256=",
"DisableSignatureVerification": true,
"Tests": [
{
"Program": "echo",
"Arguments": [ "test" ]
}
]
},
"fails_tests": {
"Script": {
"Program": "./example.sh",
"AppendPayload": true
},
"Secret": "who_cares",
"SignatureHeader": "X-Hub-Signature-256",
"SignaturePrefix": "sha256=",
"Tests": [
{
"Program": "false",
"Arguments": []
}
]
}
}
}