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", "ListenAddress": "127.0.0.1:8654",
"Services": { "Services": {
"test": { "test": {
"Script": "./example.sh", "Script": {
"Program": "./example.sh",
"AppendPayload": true
},
"Secret": "THISISVERYSECRET", "Secret": "THISISVERYSECRET",
"SignatureHeader": "X-Gitea-Signature", "SignatureHeader": "X-Gitea-Signature",
"Tests": [ "Tests": [
{ {
"Command": "git", "Program": "echo",
"Arguments": [ "pull" ] "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 #!/usr/bin/bash
date >> test_output date >> test_output
echo "$1" >> test_output

80
main.go
View File

@ -11,29 +11,19 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"os/exec"
"git.alra.uk/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 config Config var c config.Config
func main() { func main() {
r := mux.NewRouter() r := mux.NewRouter()
r.HandleFunc("/webhooks/{service}", webhookHandler) 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 { if p, ok := os.LookupEnv("CONFIG"); ok {
config_filename = p config_filename = p
} }
@ -42,7 +32,17 @@ func main() {
checkSignature = p != "true" 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) { 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 // 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 { if !ok {
writeResponse(w, 404, "Service Not Found") writeResponse(w, 404, "Service Not Found")
return return
@ -73,15 +73,15 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
// Run tests, immediately stop if one fails // Run tests, immediately stop if one fails
for _, test := range service.Tests { 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, writeResponse(w, 409,
fmt.Sprintf("409 Conflict: Test failed: %v", err.Error()), fmt.Sprintf("Conflict: Test failed: %v", err.Error()),
) )
return 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()) writeResponse(w, 500, err.Error())
return return
} else { } else {
@ -100,49 +100,3 @@ func getSha256HMACSignature(secret []byte, data string) string {
io.WriteString(h, data) io.WriteString(h, data)
return hex.EncodeToString(h.Sum(nil)) 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): After you [install go](https://golang.org/doc/install):
``` ```
go mod tidy make
go build
cp gohookr /usr/local/bin/
cp gohookr.service /usr/lib/systemd/system/
systemctl daemon-reload
systemctl enable --now gohookr
``` ```
## Signature Verification ## 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 You can disable signature verification altogether by setting environment variable
`NO_SIGNATURE_VERIFICATION` to `true`. `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. 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.
@ -40,13 +41,13 @@ A non-zero return code is considered a fail and gohookr will run no further test
deploy. 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
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 ## Example Config
Required config keys are `ListenAddress` and `Services`. 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: 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", "ListenAddress": "127.0.0.1:8654",
"Services": { "Services": {
"test": { "test": {
"Script": "./example.sh", "Script": {
"Program": "./example.sh",
"AppendPayload": true
},
"Secret": "THISISVERYSECRET", "Secret": "THISISVERYSECRET",
"SignatureHeader": "X-Gitea-Signature", "SignatureHeader": "X-Gitea-Signature",
"Tests": [ "Tests": [
{ {
"Command": "git", "Program": "echo",
"Arguments": [ "pull" ] "Arguments": [ "test" ]
} }
] ]
} }