Compare commits

...

3 Commits

Author SHA1 Message Date
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
8 changed files with 128 additions and 78 deletions

18
Makefile Normal file
View File

@ -0,0 +1,18 @@
all: install
clean:
rm -rf gohookr
install:
go mod tidy
go build -o gohookr
cp gohookr /usr/local/bin/
cp gohookr.service /usr/lib/systemd/system/
cp 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

@ -2,13 +2,16 @@
"ListenAddress": "127.0.0.1:8654",
"Services": {
"test": {
"Script": "./example.sh",
"Script": {
"Program": "./example.sh",
"AppendPayload": true
},
"Secret": "THISISVERYSECRET",
"SignatureHeader": "X-Gitea-Signature",
"Tests": [
{
"Command": "git",
"Arguments": [ "pull" ]
"Program": "echo",
"Arguments": [ "test" ]
}
]
}

19
config/command.go Normal file
View File

@ -0,0 +1,19 @@
package config
import "os/exec"
type Command struct {
Program string
Arguments []string
AppendPayload bool
}
func (c Command) Execute(payload string) ([]byte, error) {
arguments := make([]string, 0)
copy(c.Arguments, arguments)
if c.AppendPayload {
arguments = append(arguments, payload)
}
return exec.Command(c.Program, arguments...).Output()
}

39
config/config.go Normal file
View File

@ -0,0 +1,39 @@
package config
import (
"encoding/json"
"fmt"
)
type Config struct {
ListenAddress string
Services map[string]struct {
Script Command
Secret string
SignatureHeader string
Tests []Command
}
}
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 == "" {
return requiredFieldError{"SignatureHeader", serviceName}
}
if 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" >> test_output

80
main.go
View File

@ -11,29 +11,19 @@ import (
"log"
"net/http"
"os"
"os/exec"
"git.alra.uk/alvierahman90/gohookr/config"
"github.com/gorilla/mux"
)
var config_filename = "/etc/gohookr.json"
var checkSignature = true
var config Config
var c config.Config
func main() {
r := mux.NewRouter()
r.HandleFunc("/webhooks/{service}", webhookHandler)
raw_config, err := ioutil.ReadFile(config_filename)
if err != nil {
panic(err.Error())
}
config = Config{}
json.Unmarshal(raw_config, &config)
if err := config.Validate(); err != nil {
panic(err.Error())
}
if p, ok := os.LookupEnv("CONFIG"); ok {
config_filename = p
}
@ -42,7 +32,17 @@ func main() {
checkSignature = p != "true"
}
log.Fatal(http.ListenAndServe(config.ListenAddress, r))
raw_config, err := ioutil.ReadFile(config_filename)
if err != nil {
panic(err.Error())
}
json.Unmarshal(raw_config, &c)
if err := c.Validate(); err != nil {
panic(err.Error())
}
log.Fatal(http.ListenAndServe(c.ListenAddress, r))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
@ -55,7 +55,7 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
}
// check what service is specified in URL (/webhooks/{service}) and if it exists
service, ok := config.Services[string(mux.Vars(r)["service"])]
service, ok := c.Services[string(mux.Vars(r)["service"])]
if !ok {
writeResponse(w, 404, "Service Not Found")
return
@ -73,15 +73,15 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
// Run tests, immediately stop if one fails
for _, test := range service.Tests {
if _, err := exec.Command(test.Command, test.Arguments...).Output(); err != nil {
if _, err := test.Execute(payload); err != nil {
writeResponse(w, 409,
fmt.Sprintf("409 Conflict: Test failed: %v", err.Error()),
fmt.Sprintf("Conflict: Test failed: %v", err.Error()),
)
return
}
}
if stdout, err := exec.Command(service.Script, payload).Output(); err != nil {
if stdout, err := service.Script.Execute(payload); err != nil {
writeResponse(w, 500, err.Error())
return
} else {
@ -100,49 +100,3 @@ func getSha256HMACSignature(secret []byte, data string) string {
io.WriteString(h, data)
return hex.EncodeToString(h.Sum(nil))
}
func (c Config) Validate() error {
if c.ListenAddress == "" {
return requiredFieldError{"ListenAddress", ""}
}
for serviceName, service := range c.Services {
if service.Script == "" {
return requiredFieldError{"Script", serviceName}
}
if service.SignatureHeader == "" {
return requiredFieldError{"SignatureHeader", serviceName}
}
if service.Secret == "" {
return requiredFieldError{"Secret", serviceName}
}
}
return nil
}
type Test struct {
Command string
Arguments []string
}
type Service struct {
Script string
Secret string
SignatureHeader string
Tests []Test
}
type Config struct {
ListenAddress string
Services map[string]Service
}
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

@ -12,12 +12,7 @@ Check below for an example configuration.
After you [install go](https://golang.org/doc/install):
```
go mod tidy
go build
cp gohookr /usr/local/bin/
cp gohookr.service /usr/lib/systemd/system/
systemctl daemon-reload
systemctl enable --now gohookr
make
```
## Signature Verification
@ -32,7 +27,13 @@ You should also specify a shared secret in the `Secret` key.
You can disable signature verification altogether by setting environment variable
`NO_SIGNATURE_VERIFICATION` to `true`.
## Tests
## 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
gohookr can run test before running your script.
Tests must be in the form of bash scripts.
@ -40,13 +41,13 @@ A non-zero return code is considered a fail and gohookr will run no further test
deploy.
Tests are run in the order they're listed so any actions that need to be done before
real tests are run can simply be put before the tests.
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`, `Secret`, `SignatureHeader`.
Requried keys per service are `Script.Program`, `Secret`, `SignatureHeader`.
An example config file can be found [here](./config.json) but also below:
@ -55,13 +56,16 @@ An example config file can be found [here](./config.json) but also below:
"ListenAddress": "127.0.0.1:8654",
"Services": {
"test": {
"Script": "./example.sh",
"Script": {
"Program": "./example.sh",
"AppendPayload": true
},
"Secret": "THISISVERYSECRET",
"SignatureHeader": "X-Gitea-Signature",
"Tests": [
{
"Command": "git",
"Arguments": [ "pull" ]
"Program": "echo",
"Arguments": [ "test" ]
}
]
}