Compare commits

...

40 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
6cacc65013 Add config option SignaturePrefix 2021-08-04 21:43:37 +01:00
8677f5bfdd Move config to separate package 2021-08-04 12:04:01 +01:00
2189ee511c Allow executed script to have arguments 2021-08-04 11:51:40 +01:00
a2d4153c64 Create Makefile 2021-08-04 10:22:19 +01:00
294bd80cc7 Update service file to restart on app crash 2021-07-29 09:46:33 +01:00
b31f8496fc Add required field ListenAddress to config, document in readme 2021-07-29 09:42:28 +01:00
ec030052c3 Fix systemd service file... again 2021-07-29 08:48:20 +01:00
2a9bac2640 Update readme.md 2021-07-29 08:46:57 +01:00
48779bf23b Fix systemd service file 2021-07-29 08:45:12 +01:00
372171fb86 Add installation instruction, systemd service file 2021-07-29 08:43:06 +01:00
fd93cc4fb1 fix signature comparison logic 2021-07-29 08:29:37 +01:00
3fa2c958f7 Add Dockerfile 2021-07-29 08:09:04 +01:00
76197520e7 Make enpoint prefix plural, change stuff in readme 2021-07-29 07:54:22 +01:00
b4d9935e39 Add example config to readme 2021-07-29 07:44:14 +01:00
4691ea5cc3 Rewrite how tests are defined 2021-07-29 07:42:02 +01:00
a82726ca52 Update readme.md 2021-07-29 07:37:25 +01:00
c99a5943aa Only execute if all tests are successful 2021-07-29 07:37:25 +01:00
c0f0cc6c60 Clean up code 2021-07-29 07:37:25 +01:00
242ae06cad Remove testing related code 2021-07-29 07:37:25 +01:00
cd2c0ff0fe Add ability to disable signature verification 2021-07-29 07:37:25 +01:00
9fca2cefa3 Fix module name 2021-07-29 07:37:18 +01:00
12 changed files with 320 additions and 68 deletions

2
.gitignore vendored Normal file
View File

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

9
Dockerfile Normal file
View File

@@ -0,0 +1,9 @@
FROM golang:1.16
WORKDIR /go/src/app
COPY . .
RUN go get -d -v ./...
RUN go install -v ./...
CMD ["gohookr"]

20
Makefile Normal file
View File

@@ -0,0 +1,20 @@
all: install
clean:
rm -rf gohookr
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
systemctl daemon-reload
systemctl enable --now gohookr
uninstall:
systemctl disable --now gohookr
rm -rf /usr/local/bin/gohookr /usr/lib/systemd/system/gohookr.service
systemctl daemon-reload

View File

@@ -1,9 +1,34 @@
{
"ListenAddress": "127.0.0.1:8654",
"Services": {
"test": {
"Script": "./example.sh",
"Secret": "THISISVERYSECRET",
"SignatureHeader": "X-Gitea-Signature"
"Script": {
"Program": "./example.sh",
"AppendPayload": true,
"AppendHeaders": true
},
"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": []
}
]
}
}
}

41
config/command.go Normal file
View File

@@ -0,0 +1,41 @@
package config
import (
"fmt"
"net/http"
"os/exec"
"strings"
)
type Command struct {
Program string
Arguments []string
AppendPayload bool
AppendHeaders bool
}
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,
)
}

35
config/config.go Normal file
View File

@@ -0,0 +1,35 @@
package config
// The struct that represents the config.json file
type Config struct {
ListenAddress string
Services map[string]struct {
Script Command
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", ""}
}
for serviceName, service := range c.Services {
if service.Script.Program == "" {
return requiredFieldError{"Script.Program", serviceName}
}
if !service.DisableSignatureVerification && service.SignatureHeader == "" {
return requiredFieldError{"SignatureHeader", serviceName}
}
if !service.DisableSignatureVerification && service.Secret == "" {
return requiredFieldError{"Secret", serviceName}
}
}
return nil
}

12
config/errors.go Normal file
View File

@@ -0,0 +1,12 @@
package config
import "fmt"
type requiredFieldError struct {
fieldName string
serviceName string
}
func (e requiredFieldError) Error() string {
return fmt.Sprintf("%v cannot be empty (%v)", e.fieldName, e.serviceName)
}

View File

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

2
go.mod
View File

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

11
gohookr.service Normal file
View File

@@ -0,0 +1,11 @@
[Unit]
Description=A really simple webhook receiver.
After=network.target
[Service]
Restart=on-failure
RestartSec=5s
ExecStart=/usr/local/bin/gohookr
[Install]
WantedBy=multi-user.target

119
main.go
View File

@@ -7,86 +7,101 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"git.alv.cx/alvierahman90/gohookr/config"
"github.com/gorilla/mux"
)
var config_filename = "/etc/ghookr.json"
var config_filename = "/etc/gohookr.json"
var checkSignature = true
var c config.Config
func main() {
// Used for testing purposes... generates hmac string
if os.Getenv("HMACGEN") == "true" {
input, err := ioutil.ReadAll(os.Stdin)
secret := os.Getenv("SECRET")
if err != nil {
panic(err.Error())
}
fmt.Println(getSha256HMACSignature([]byte(secret), string(input)))
return
}
r := mux.NewRouter()
r.HandleFunc("/webhook/{service}", webhook)
port := ":80"
if p, ok := os.LookupEnv("PORT"); ok {
port = fmt.Sprintf(":%v", p)
}
r.HandleFunc("/webhooks/{service}", webhookHandler)
if p, ok := os.LookupEnv("CONFIG"); ok {
config_filename = p
}
log.Fatal(http.ListenAndServe(port, r))
if p, ok := os.LookupEnv("NO_SIGNATURE_CHECK"); ok {
checkSignature = p != "true"
}
func webhook(w http.ResponseWriter, r *http.Request) {
// TODO run any specified tests before running script
raw_config, err := os.ReadFile(config_filename)
if err != nil {
panic(err.Error())
}
service_name := mux.Vars(r)["service"]
if err := json.Unmarshal(raw_config, &c); err != nil {
panic(err.Error())
}
if err := c.Validate(); err != nil {
panic(err.Error())
}
log.Fatal(http.ListenAndServe(c.ListenAddress, r))
}
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)
}
raw_config, err := ioutil.ReadFile(config_filename)
if err != nil {
writeResponse(w, 500, "Internal Server Error: Could not open config file")
return
}
config := Config{}
json.Unmarshal(raw_config, &config)
var service = Service{}
if val, ok := config.Services[string(service_name)]; !ok {
writeResponse(w, 404, "Service Not Found")
return
} else {
service = val
}
// Verify that signature provided matches signature calculated using secretsss
signature := r.Header.Get(service.SignatureHeader)
if signature == getSha256HMACSignature([]byte(service.Secret), payload) {
calculatedSignature := fmt.Sprintf(
"%v%v",
service.SignaturePrefix,
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")
fmt.Println("Signatures do not match!")
return
}
if stdout, err := exec.Command(service.Script, payload).Output(); err != nil {
writeResponse(w, 500, err.Error())
return
} else {
writeResponse(w, 200, string(stdout))
// 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, r.Header); err != nil {
fmt.Printf("Test failed(%v) for service %v\n", test, serviceName)
return
}
}
stdout, err := service.Script.Execute(payload, r.Header)
fmt.Println(string(stdout))
if err != nil {
fmt.Println(err.Error())
}
}()
writeResponse(w, 200, "OK")
return
}
func writeResponse(w http.ResponseWriter, responseCode int, responseString string) {
w.WriteHeader(responseCode)
@@ -98,15 +113,3 @@ func getSha256HMACSignature(secret []byte, data string) string {
io.WriteString(h, data)
return hex.EncodeToString(h.Sum(nil))
}
type Service struct {
Gitea bool
Script string
Secret string
SignatureHeader string
Tests []string
}
type Config struct {
Services map[string]Service
}

View File

@@ -1,7 +1,100 @@
# gohookr
A _really_ simple webhook receiver.
A _really_ simple webhook receiver, which listens at `/webhooks/<webhook-name>`.
Check config.json for an example configuration.
## Installation
Default config path is `/etc/ghookr.conf`, can be overriden with `CONFIG` environment variable.
After you [install go](https://golang.org/doc/install):
```
make
```
## 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`
key for each service.
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
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
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 (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
gohookr can run test before running your script.
Tests must be in the form of bash scripts.
A non-zero return code is considered a fail and gohookr will run no further tests and will not
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
Required config keys are `ListenAddress` and `Services`.
Requried keys per service are `Script.Program`, `Secret`, `SignatureHeader`.
An example config file can be found [here](./config.json) but also below:
```json
{
"ListenAddress": "127.0.0.1:8654",
"Services": {
"test": {
"Script": {
"Program": "./example.sh",
"AppendPayload": true,
"AppendHeaders": true
},
"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": []
}
]
}
}
}
```