Add testing

Adds a blackbox test and fixes some discovered bugs in the program
argument handling. Also adds the `Output` option to web hooks.
This commit is contained in:
Felix Niederwanger 2022-12-29 16:03:11 +01:00
parent 889f2e67cf
commit d7589937b6
Signed by: phoenix
GPG key ID: 31860289A704FB3C
6 changed files with 159 additions and 14 deletions

View file

@ -6,3 +6,6 @@ weblug: cmd/weblug/weblug.go cmd/weblug/config.go
static: cmd/weblug/weblug.go cmd/weblug/config.go
CGO_ENABLED=0 go build -ldflags="-w -s" -o weblug $^
test: weblug
sudo bash -c "cd test && ./blackbox.sh"

View file

@ -4,7 +4,6 @@ import (
"fmt"
"io/ioutil"
"os/exec"
"strings"
"sync/atomic"
"syscall"
@ -27,8 +26,9 @@ type Hook struct {
Background bool `yaml:"background"` // Run in background
Concurrency int `yaml:"concurrency"` // Number of allowed concurrent runs
concurrentRuns int32 // Number of current concurrent runs
UID int `yaml:"uid"` // UID to use when running the command
GID int `yaml:"gid"` // GID to use when running the command
UID int `yaml:"uid"` // UID to use when running the command
GID int `yaml:"gid"` // GID to use when running the command
Output bool `yaml:"output"` // Print program output
}
func (cf *Config) SetDefaults() {
@ -83,22 +83,56 @@ func (hook *Hook) Unlock() {
atomic.AddInt32(&hook.concurrentRuns, -1)
}
// Split a command into program arguments, obey quotation mark escapes
func cmdSplit(command string) []string {
null := rune(0)
esc := null // Escape character or \0 if not escaped currently
ret := make([]string, 0)
buf := "" // Current command
for _, char := range command {
if esc != null {
if char == esc {
esc = null
} else {
buf += string(char)
}
} else {
// Check for quotation marks
if char == '\'' || char == '"' {
esc = char
} else if char == ' ' {
ret = append(ret, buf)
buf = ""
} else {
buf += string(char)
}
}
}
// Remaining characters
if buf != "" {
ret = append(ret, buf)
buf = ""
}
return ret
}
// Run executes the given command and return it's return code. It also respects the given concurrency number and will block until resources are free
func (hook *Hook) Run() error {
split := strings.Split(hook.Command, " ")
split := cmdSplit(hook.Command)
args := make([]string, 0)
if len(split) > 1 {
args = split[1:]
}
cmd := exec.Command(split[0], args...)
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{}
if hook.UID > 0 {
cmd.SysProcAttr.Credential.Uid = uint32(hook.UID)
if hook.UID > 0 || hook.GID > 0 {
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(hook.UID), Gid: uint32(hook.GID)}
}
if hook.GID > 0 {
cmd.SysProcAttr.Credential.Gid = uint32(hook.GID)
if hook.Output {
buf, ret := cmd.Output()
fmt.Println(string(buf))
return ret
}
ret := cmd.Run()
return ret
return cmd.Run()
}

View file

@ -36,6 +36,21 @@ func awaitTerminationSignal() {
}()
}
// Perform sanity check on hooks
func sanityCheckHooks(hooks []Hook) error {
uid := os.Getuid()
for _, hook := range hooks {
// If a hook sets a custom uid or gid, ensure we're running as root, otherwise print a warning
if hook.UID != 0 && uid != 0 {
fmt.Fprintf(os.Stderr, "Warning: Hook '%s' sets 'uid = %d' but we're not running as root\n", hook.Name, hook.UID)
}
if hook.GID != 0 && uid != 0 {
fmt.Fprintf(os.Stderr, "Warning: Hook '%s' sets 'gid = %d' but we're not running as root\n", hook.Name, hook.GID)
}
}
return nil
}
func main() {
cf.SetDefaults()
if len(os.Args) < 2 {
@ -60,6 +75,12 @@ func main() {
os.Exit(2)
}
// Sanity check
if err := sanityCheckHooks(cf.Hooks); err != nil {
fmt.Fprintf(os.Stderr, "hook sanity check failed: %s\n", err)
os.Exit(3)
}
// Create default handlers
http.HandleFunc("/health", createHealthHandler())
http.HandleFunc("/health.json", createHealthHandler())

59
test/blackbox.sh Executable file
View file

@ -0,0 +1,59 @@
#!/bin/bash
# Blackbox tests for weblug
cleanup() {
# kill all processes whose parent is this process
pkill -P $$
}
trap cleanup EXIT
if [[ $EUID != 0 && $UID != 0 ]]; then
echo -e "(!!) WARNING: Cannot UID and GID webhook tests, because we're not running as root (!!)\n"
echo -e "Continuing in 3 seconds\n\n\n"
sleep 3
fi
rm -f testfile
../weblug test.yaml &
sleep 1
## Check touch webhook, which creates "testfile"
echo -e "\n\nChecking 'testfile' webhook ... "
curl http://127.0.0.1:2088/webhooks/touch
if [[ ! -f testfile ]]; then
echo "Testfile doesn't exist after running webhook touch"
exit 1
fi
rm -f testfile
## Check background webhook, that sleeps for 5 seconds but returns immediately
echo -e "\n\nChecking 'background' webhook ... "
timeout 2 curl http://127.0.0.1:2088/webhooks/background
## Check concurrency webhook, that allows only 2 requests at the same time (but sleeps for 5 seconds)
echo -e "\n\nChecking 'concurrency' webhook ... "
timeout 10 curl http://127.0.0.1:2088/3 &
timeout 10 curl http://127.0.0.1:2088/3 &
! timeout 2 curl http://127.0.0.1:2088/3
## Check UID and GID webhooks, but only if we're root
echo -e "\n\nChecking 'uid/gid' webhook ... "
# Skip this test, if we're not root
if [[ $EUID == 0 || $UID == 0 ]]; then
curl http://127.0.0.1:2088/webhooks/uid
curl http://127.0.0.1:2088/webhooks/gid
else
echo "Cannot UID and GID webhook tests, because we're not running as root"
fi
echo -e "\n\nall good"

27
test/test.yaml Normal file
View file

@ -0,0 +1,27 @@
---
settings:
bind: "127.0.0.1:2088" # bind address for webserver
hooks:
- name: 'touch hook'
route: "/webhooks/touch"
command: "touch testfile"
- name: 'hook background'
route: "/webhooks/background"
command: "sleep 5"
background: True
- name: 'hook three'
route: "/3"
command: "sleep 5"
concurrency: 2
- name: 'hook uid'
route: "/webhooks/uid"
command: "bash -c 'echo uid=$UID gid=$GID; if [[ $UID != 10 ]]; then exit 1; fi'"
uid: 10
output: True
- name: 'hook gid'
route: "/webhooks/gid"
command: "bash -c 'GID=`id -g`; echo uid=$UID gid=$GID; if [[ $GID != 10 ]]; then exit 1; fi'"
uid: 10
gid: 10
output: True

View file

@ -15,11 +15,12 @@ hooks:
concurrency: 2 # At most 2 parallel processes are allowed
- name: 'hook two'
route: "/webhooks/2"
command: "sleep 5"
command: "bash -c 'sleep 5'"
concurrency: 5 # At most 5 parallel processes are allowed
- name: 'hook 3'
route: "/webhooks/data/3"
command: "/srv/fetch-new-data.sh"
command: "bash -c 'echo $UID $GID'"
uid: 100 # Run command as system user id (uid) 100
gid: 200 # Run command with system group id (gid) 200
concurrency: 1 # No concurrency. Returns 500 on parallel requests
output: True # Print program output to console