Compare commits

...

19 Commits

Author SHA1 Message Date
7c778d4e48 fix program arguments not being passed to commands 2024-08-17 12:42:37 +01:00
f13dbe3250 include appendheaders in readme example config 2023-10-28 22:41:38 +01:00
c5fef8e42d AppendHeaders 2023-10-28 21:48:54 +01:00
e74c3a684e stop using deprecated ioutil 2023-10-28 21:47:35 +01:00
0f8b4d2e1e update gitignore 2023-10-28 21:46:58 +01:00
f3cdde5fe6 update makefile 2023-10-28 21:46:15 +01:00
cdd19d95e9 closes #1, closes #3 update readme 2022-02-12 13:28:28 +00:00
dfd44b7ea1 update module path thingy 2022-01-21 21:08:05 +00:00
1e53930e8c Check if config is valid JSON 2021-09-01 18:58:18 +01:00
3cadcfbabe Fix signature logic 2021-08-14 01:06:29 +01:00
39fe4748e1 Add support for individually disabling signature verification 2021-08-14 01:02:36 +01:00
f2b2ac9368 update example config 2021-08-06 14:54:36 +01:00
4c8cb33c59 Better logging, comments 2021-08-06 14:53:22 +01:00
cf65488907 Implement Stringer in Command type 2021-08-06 14:29:24 +01:00
87ea4cc5e5 Don't print config on start, add a couple comments 2021-08-06 14:27:42 +01:00
5ab36c57ef convert stdout to string before printing 2021-08-04 22:18:53 +01:00
a3ded5a052 Run tests and script in parralel to prevent timing out 2021-08-04 22:17:43 +01:00
081aaee9c7 Actually print calcuated signature to stdout 2021-08-04 22:12:59 +01:00
0953baae50 Don't overwrite existing user configs 2021-08-04 22:12:38 +01:00
9 changed files with 144 additions and 74 deletions

2
.gitignore vendored Normal file
View File

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

View File

@@ -3,12 +3,14 @@ all: install
clean: clean:
rm -rf gohookr rm -rf gohookr
install: build:
go mod tidy go mod tidy
go build -o gohookr go build -o gohookr
install: build
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

View File

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

View File

@@ -1,19 +1,41 @@
package config package config
import "os/exec" import (
"fmt"
"net/http"
"os/exec"
"strings"
)
type Command struct { type Command struct {
Program string Program string
Arguments []string Arguments []string
AppendPayload bool AppendPayload bool
AppendHeaders bool
} }
func (c Command) Execute(payload string) ([]byte, error) { func (c Command) Execute(payload string, header http.Header) ([]byte, error) {
arguments := make([]string, 0) arguments := make([]string, len(c.Arguments))
copy(c.Arguments, arguments) copy(arguments, c.Arguments)
if c.AppendPayload { if c.AppendPayload {
arguments = append(arguments, payload) 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() 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,37 +1,32 @@
package config package config
import ( // The struct that represents the config.json file
"encoding/json"
"fmt"
)
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}
} }
} }

View File

@@ -1,3 +1,3 @@
#!/usr/bin/bash #!/usr/bin/bash
date >> test_output 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 go 1.16

75
main.go
View File

@@ -7,12 +7,11 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"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"
) )
@@ -32,12 +31,15 @@ func main() {
checkSignature = p != "true" checkSignature = p != "true"
} }
raw_config, err := ioutil.ReadFile(config_filename) raw_config, err := os.ReadFile(config_filename)
if err != nil { if err != nil {
panic(err.Error()) 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 { if err := c.Validate(); err != nil {
panic(err.Error()) panic(err.Error())
} }
@@ -46,48 +48,59 @@ func main() {
} }
func webhookHandler(w http.ResponseWriter, r *http.Request) { 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 := "" 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") 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, r.Header); err != nil {
) fmt.Printf("Test failed(%v) for service %v\n", test, serviceName)
return return
}
} }
} stdout, err := service.Script.Execute(payload, r.Header)
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,17 @@ 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`.
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. 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 +30,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 (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. 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 +54,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`.
@@ -61,17 +69,31 @@ An example config file can be found [here](./config.json) but also below:
"test": { "test": {
"Script": { "Script": {
"Program": "./example.sh", "Program": "./example.sh",
"AppendPayload": true "AppendPayload": true,
"AppendHeaders": 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": []
}
]
} }
} }
} }