Compare commits

..

15 Commits

9 changed files with 125 additions and 62 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
gohookr
test_output

View File

@@ -3,9 +3,11 @@ all: install
clean:
rm -rf gohookr
install:
build:
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

View File

@@ -4,17 +4,31 @@
"test": {
"Script": {
"Program": "./example.sh",
"AppendPayload": true
"AppendPayload": true,
"AppendHeaders": 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": []
}
]
}
}
}

View File

@@ -1,19 +1,41 @@
package config
import "os/exec"
import (
"fmt"
"net/http"
"os/exec"
"strings"
)
type Command struct {
Program string
Arguments []string
AppendPayload bool
AppendHeaders bool
}
func (c Command) Execute(payload string) ([]byte, error) {
arguments := make([]string, 0)
copy(c.Arguments, arguments)
func (c Command) Execute(payload string, header http.Header) ([]byte, error) {
arguments := make([]string, len(c.Arguments))
copy(arguments, c.Arguments)
if c.AppendPayload {
arguments = append(arguments, payload)
}
if c.AppendHeaders {
var header_builder strings.Builder;
header.Write(&header_builder);
arguments = append(arguments, header_builder.String())
}
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

@@ -1,10 +1,6 @@
package config
import (
"encoding/json"
"fmt"
)
// The struct that represents the config.json file
type Config struct {
ListenAddress string
Services map[string]struct {
@@ -12,26 +8,25 @@ 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}
}
}

View File

@@ -1,3 +1,3 @@
#!/usr/bin/bash
date >> test_output
echo "$1" >> test_output
echo "$1" "$2" >> test_output

2
go.mod
View File

@@ -1,4 +1,4 @@
module git.alra.uk/alvierahman90/gohookr
module git.alv.cx/alvierahman90/gohookr
go 1.16

46
main.go
View File

@@ -7,12 +7,11 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"git.alra.uk/alvierahman90/gohookr/config"
"git.alv.cx/alvierahman90/gohookr/config"
"github.com/gorilla/mux"
)
@@ -32,12 +31,15 @@ func main() {
checkSignature = p != "true"
}
raw_config, err := ioutil.ReadFile(config_filename)
raw_config, err := os.ReadFile(config_filename)
if err != nil {
panic(err.Error())
}
json.Unmarshal(raw_config, &c)
if err := json.Unmarshal(raw_config, &c); err != nil {
panic(err.Error())
}
if err := c.Validate(); err != nil {
panic(err.Error())
}
@@ -46,21 +48,26 @@ func main() {
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
// 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 {
if p, err := io.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,23 +77,22 @@ 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()),
)
if _, err := test.Execute(payload, r.Header); err != nil {
fmt.Printf("Test failed(%v) for service %v\n", test, serviceName)
return
}
}
stdout, err := service.Script.Execute(payload)
stdout, err := service.Script.Execute(payload, r.Header)
fmt.Println(string(stdout))
if err != nil {
fmt.Println(err.Error())

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,17 @@ 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`.
Check below for an example configuration, which should tell you most of the things you need to know
to configure gohookr.
Currently gohookr must be restarted after config changes.
### 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 +30,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.
You can ask it to put the payload as the last (or second to last if `AppendHeaders` is set) argument by setting `AppendPayload` to true.
You can ask it to put the request headers as the last argument by setting `AppendHeaders` 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 +54,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`.
@@ -61,17 +69,31 @@ An example config file can be found [here](./config.json) but also below:
"test": {
"Script": {
"Program": "./example.sh",
"AppendPayload": true
"AppendPayload": true,
"AppendHeaders": 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": []
}
]
}
}
}