Compare commits

...

15 Commits

11 changed files with 184 additions and 68 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
gohookr

View File

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

View File

@@ -6,15 +6,28 @@
"Program": "./example.sh", "Program": "./example.sh",
"AppendPayload": true "AppendPayload": true
}, },
"Secret": "THISISVERYSECRET", "DisableSignatureVerification": true,
"SignatureHeader": "X-Hub-Signature",
"SignaturePrefix": "sha256=",
"Tests": [ "Tests": [
{ {
"Program": "echo", "Program": "echo",
"Arguments": [ "test" ] "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 package config
import "os/exec" import (
"fmt"
"os/exec"
)
type Command struct { type Command struct {
Program string Program string
@@ -17,3 +20,11 @@ func (c Command) Execute(payload string) ([]byte, error) {
return exec.Command(c.Program, arguments...).Output() 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,39 +2,61 @@ package config
import ( import (
"encoding/json" "encoding/json"
"fmt" "io/ioutil"
"gopkg.in/yaml.v3"
) )
// The struct that represents the config.json file
type Config struct { type Config struct {
ListenAddress string ListenAddress string
Services map[string]struct { Services map[string]struct {
Script Command Script Command
Secret string Secret string
SignaturePrefix string SignaturePrefix string
SignatureHeader string SignatureHeader string
Tests []Command DisableSignatureVerification bool
Tests []Command
} }
} }
// Check that all required fields are filled in
func (c Config) Validate() error { func (c Config) Validate() error {
if c.ListenAddress == "" { if c.ListenAddress == "" {
return requiredFieldError{"ListenAddress", ""} return requiredFieldError{"ListenAddress", ""}
} }
jsonbytes, _ := json.MarshalIndent(c, "", " ")
fmt.Println(string(jsonbytes))
for serviceName, service := range c.Services { for serviceName, service := range c.Services {
if service.Script.Program == "" { if service.Script.Program == "" {
return requiredFieldError{"Script.Program", serviceName} return requiredFieldError{"Script.Program", serviceName}
} }
if service.SignatureHeader == "" { if !service.DisableSignatureVerification && service.SignatureHeader == "" {
return requiredFieldError{"SignatureHeader", serviceName} return requiredFieldError{"SignatureHeader", serviceName}
} }
if service.Secret == "" { if !service.DisableSignatureVerification && service.Secret == "" {
return requiredFieldError{"Secret", serviceName} return requiredFieldError{"Secret", serviceName}
} }
} }
return nil 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 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 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 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=

85
main.go
View File

@@ -4,7 +4,6 @@ import (
"crypto/hmac" "crypto/hmac"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@@ -12,15 +11,16 @@ import (
"net/http" "net/http"
"os" "os"
"git.alra.uk/alvierahman90/gohookr/config" "git.alv.cx/alvierahman90/gohookr/config"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
var config_filename = "/etc/gohookr.json" var config_filename = "/etc/gohookr.json"
var checkSignature = true var checkSignature = true
var c config.Config
func main() { func main() {
var c config.Config
r := mux.NewRouter() r := mux.NewRouter()
r.HandleFunc("/webhooks/{service}", webhookHandler) r.HandleFunc("/webhooks/{service}", webhookHandler)
@@ -32,62 +32,81 @@ func main() {
checkSignature = p != "true" checkSignature = p != "true"
} }
raw_config, err := ioutil.ReadFile(config_filename) var err = c.Load(config_filename)
if err != nil { if err != nil {
panic(err.Error()) panic(err.Error())
} }
fmt.Printf("CONFIG OK: %s\n", config_filename)
fmt.Printf("LISTENING AT: %s\n", c.ListenAddress)
json.Unmarshal(raw_config, &c) for _, v := range os.Args {
if err := c.Validate(); err != nil { if v == "checkConfig" {
panic(err.Error()) return
}
} }
log.Fatal(http.ListenAndServe(c.ListenAddress, r)) log.Fatal(http.ListenAndServe(c.ListenAddress, r))
} }
func webhookHandler(w http.ResponseWriter, r *http.Request) { 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 := "" payload := ""
if p, err := ioutil.ReadAll(r.Body); err != nil { if p, err := ioutil.ReadAll(r.Body); err != nil {
writeResponse(w, 500, "Internal Server Error: Could not read payload") writeResponse(w, 500, "Internal Server Error: Could not read payload")
fmt.Println("Error: Could not read payload")
return return
} else { } else {
payload = string(p) 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 // Verify that signature provided matches signature calculated using secretsss
signature := r.Header.Get(service.SignatureHeader) signature := r.Header.Get(service.SignatureHeader)
calculatedSignature := fmt.Sprintf("%v%v", service.SignaturePrefix, getSha256HMACSignature([]byte(service.Secret), payload)) calculatedSignature := fmt.Sprintf(
fmt.Printf("signature = %v\n", signature) "%v%v",
fmt.Printf("calcuatedSignature = %v\n", signature) service.SignaturePrefix,
if signature != calculatedSignature && checkSignature { getSha256HMACSignature([]byte(service.Secret), payload),
)
fmt.Printf("signature = %v\n", signature)
fmt.Printf("calcuatedSignature = %v\n", calculatedSignature)
if checkSignature && !service.DisableSignatureVerification && signature != calculatedSignature {
writeResponse(w, 400, "Bad Request: Signatures do not match") writeResponse(w, 400, "Bad Request: Signatures do not match")
fmt.Println("Signatures do not match!")
return return
} }
// Run tests, immediately stop if one fails // Run tests and script as goroutine to prevent timing out
for _, test := range service.Tests { go func() {
if _, err := test.Execute(payload); err != nil { // Run tests, immediately stop if one fails
writeResponse(w, 409, for _, test := range service.Tests {
fmt.Sprintf("Conflict: Test failed: %v", err.Error()), if _, err := test.Execute(payload); err != nil {
) fmt.Printf("Test failed(%v) for service %v\n", test, serviceName)
return return
}
} }
} stdout, err := service.Script.Execute(payload)
fmt.Println(string(stdout))
if err != nil {
fmt.Println(err.Error())
}
}()
if stdout, err := service.Script.Execute(payload); err != nil { writeResponse(w, 200, "OK")
writeResponse(w, 500, err.Error()) return
return
} else {
writeResponse(w, 200, string(stdout))
return
}
} }
func writeResponse(w http.ResponseWriter, responseCode int, responseString string) { func writeResponse(w http.ResponseWriter, responseCode int, responseString string) {

View File

@@ -2,11 +2,6 @@
A _really_ simple webhook receiver, which listens at `/webhooks/<webhook-name>`. 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 ## Installation
After you [install go](https://golang.org/doc/install): After you [install go](https://golang.org/doc/install):
@@ -15,7 +10,24 @@ After you [install go](https://golang.org/doc/install):
make 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. Signature verificaiton is done using SHA256 HMACs.
You **must** set which HTTP header gohookr will receive a signature from using the `SignatureHeader` 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`. You may also need to specify a `SignaturePrefix`.
For GitHub it would be `sha256=`. 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`. `NO_SIGNATURE_VERIFICATION` to `true`.
## Writing Commands ### Writing Commands
gohookr doesn't care what the command is as long as the `Program` is executable. 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 specify extra arguments with the `Arguments` field.
You can ask it to put the payload as the last argument by setting `AppendPayload` to true. 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. gohookr can run test before running your script.
Tests must be in the form of bash scripts. 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 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. tests are run can simply be put in this section before the tests.
## Example Config ### Example Config
Required config keys are `ListenAddress` and `Services`. 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", "Program": "./example.sh",
"AppendPayload": true "AppendPayload": true
}, },
"Secret": "THISISVERYSECRET", "DisableSignatureVerification": true,
"SignatureHeader": "X-Hub-Signature",
"SignaturePrefix": "sha256=",
"Tests": [ "Tests": [
{ {
"Program": "echo", "Program": "echo",
"Arguments": [ "test" ] "Arguments": [ "test" ]
} }
] ]
},
"fails_tests": {
"Script": {
"Program": "./example.sh",
"AppendPayload": true
},
"Secret": "who_cares",
"SignatureHeader": "X-Hub-Signature-256",
"SignaturePrefix": "sha256=",
"Tests": [
{
"Program": "false",
"Arguments": []
}
]
} }
} }
} }