File weblug-0.4.obscpio of Package weblug
07070100000000000081A4000000000000000000000001647EF371000001F1000000000000000000000000000000000000001600000000weblug-0.4/.gitignore# ---> Go
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
/weblug
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
07070100000001000081A4000000000000000000000001647EF37100000436000000000000000000000000000000000000001300000000weblug-0.4/LICENSEMIT License
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
07070100000002000081A4000000000000000000000001647EF371000000DD000000000000000000000000000000000000001400000000weblug-0.4/Makefiledefault: all
all: weblug
.PHONY: test
weblug: cmd/weblug/*.go
go build -o weblug $^
static: cmd/weblug/*.go
CGO_ENABLED=0 go build -ldflags="-w -s" -o weblug $^
test: weblug
sudo bash -c "cd test && ./blackbox.sh"
07070100000003000081A4000000000000000000000001647EF37100000821000000000000000000000000000000000000001500000000weblug-0.4/README.md# weblug
Webhook receiver program. `weblug` is is a configurable webhook receiver that allows users to define custom programs and script to be executed when a webhook is triggered.
The configuration happens via a [yaml file](weblug.yml). Read the [usage caveats](#caveats)!
`weblug` supports multiple webhooks, limitations for concurrent web hooks to be executed, background execution and running webhooks as separate user (`uid`/`gid`).
## Usage
To use `weblug` you need to define webooks in a yaml file. See [weblug.yml](weblug.yml) for an example configuration. Then run
./weblug YAML-FILE
This starts the webserver and listens for incoming webhooks.
`weblug` can run as any user, however for custom `uid`/`gid` webhooks, the program needs to run as root.
### Caveats
1. `weblug` does not support https encryption!
weblug is expected to run behind a http reverse proxy (e.g. `apache` or `nginx`) which handles transport encryption. The program it self does not support https, nor are there any plans to implement this in the near future.
CAVE: Don't expose secrets and credentials by running this without any transport encryption!
2. Do not run this without reverse proxy
`weblug` relies on the standart go http implementation. To avoid a whole class of securtiy concerns, `weblug` should never run on the open internet without a http reverse proxy.
3. `weblug` runs as root, when using custom UID/GIDs
In it's current implementation, `weblug` requires to remain running as root without dropping privileges when using custom UID/GIDs. This is a current limitation that will be hopefully resolved soon.
## Build
make # Build weblug
make static # Make a static binary
## Run as systemd unit
This repository provides an example [weblug.service](weblug.service), which can be used to start `weblug` as systemd service.
This file can be placed in `/etc/systemd/system/weblug.service` and in conjunction with an adequate `weblug.yml` file e.g. in `/etc/weblug.yml` this provides a way of running weblug as a native systemd service.07070100000004000041ED000000000000000000000001647EF37100000000000000000000000000000000000000000000000F00000000weblug-0.4/cmd07070100000005000041ED000000000000000000000001647EF37100000000000000000000000000000000000000000000001600000000weblug-0.4/cmd/weblug07070100000006000081A4000000000000000000000001647EF371000004D5000000000000000000000000000000000000002000000000weblug-0.4/cmd/weblug/config.gopackage main
import (
"fmt"
"io/ioutil"
"gopkg.in/yaml.v2"
)
type Config struct {
Settings ConfigSettings `yaml:"settings"`
Hooks []Hook `yaml:"hooks"`
}
type ConfigSettings struct {
BindAddress string `yaml:"bind"` // Bind address for the webserver
UID int `yaml:"uid"` // Custom user ID or 0, if not being used
GID int `yaml:"gid"` // Custom group ID or 0, if not being used
}
func (cf *Config) SetDefaults() {
cf.Settings.BindAddress = ":2088"
}
// Check performs sanity checks on the config
func (cf *Config) Check() error {
if cf.Settings.BindAddress == "" {
return fmt.Errorf("no bind address configured")
}
for _, hook := range cf.Hooks {
if hook.Name == "" {
return fmt.Errorf("hook without name")
}
if hook.Route == "" {
return fmt.Errorf("hook %s with no route", hook.Name)
}
if hook.Command == "" {
return fmt.Errorf("hook %s with no command", hook.Name)
}
if hook.Concurrency < 1 {
hook.Concurrency = 1
}
}
return nil
}
func (cf *Config) LoadYAML(filename string) error {
content, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
if err := yaml.Unmarshal(content, cf); err != nil {
return err
}
return cf.Check()
}
07070100000007000081A4000000000000000000000001647EF37100001620000000000000000000000000000000000000001E00000000weblug-0.4/cmd/weblug/hook.gopackage main
import (
"fmt"
"net"
"os/exec"
"strings"
"sync/atomic"
"syscall"
)
type Hook struct {
Name string `yaml:"name"` // name of the hook
Route string `yaml:"route"` // http route
Command string `yaml:"command"` // Actual command to execute
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
Output bool `yaml:"output"` // Print program output
AllowAddresses []string `yaml:"allowed"` // Addresses that are explicitly allowed
BlockedAddresses []string `yaml:"blocked"` // Addresses that are explicitly blocked
HttpBasicAuth BasicAuth `yaml:"basic_auth"` // Optional requires http basic auth
Env map[string]string `yaml:"env"` // Optional environment variables
}
type BasicAuth struct {
Username string `yaml:"username"` // Optional: Required username for the webhook to be allowed. If empty, any username will be accepted
Password string `yaml:"password"` // If set, the http basic auth is enabled and the request must contain this password for being allowed
}
// Tries to lock a spot. Returns false, if the max. number of concurrent runs has been reached
func (hook *Hook) TryLock() bool {
res := int(atomic.AddInt32(&hook.concurrentRuns, 1))
if res > hook.Concurrency {
atomic.AddInt32(&hook.concurrentRuns, -1)
return false
}
return true
}
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 := cmdSplit(hook.Command)
args := make([]string, 0)
if len(split) > 1 {
args = split[1:]
}
cmd := exec.Command(split[0], args...)
if hook.UID > 0 || hook.GID > 0 {
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(hook.UID), Gid: uint32(hook.GID)}
}
cmd.Env = make([]string, 0)
if hook.Env != nil {
// Build environment variable list as expected by cmd.Env
for k, v := range hook.Env {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
}
}
if hook.Output {
buf, ret := cmd.Output()
fmt.Println(string(buf))
return ret
}
return cmd.Run()
}
func isAddressInList(addr string, addrList []string) (bool, error) {
ip, _, err := net.ParseCIDR(addr)
if err != nil {
return false, err
}
for _, item := range addrList {
iAddr, iNet, err := net.ParseCIDR(item)
if err != nil {
return false, err
}
if ip.Equal(iAddr) {
return true, nil
}
if iNet.Contains(ip) {
return true, nil
}
}
return false, nil
}
// Extract only the CIDR address from the given address identifier. This removes the port, if present
func cidr(addr string) string {
// Check for IPv6 address
s, e := strings.Index(addr, "["), strings.Index(addr, "]")
if s >= 0 && e > 0 {
return addr[s+1:e] + "/128"
}
// Simply remove the port
i := strings.Index(addr, ":")
if i > 0 {
return addr[:i-1] + "/32"
}
return addr + "/32"
}
// IsAddressAllowed checks if the hook allows the given address. An address is allowed, if it is present in the AllowAddresses list (if non-empty) and if it is not present in the BlockedAddresses list (if non-empty)
func (hook *Hook) IsAddressAllowed(addr string) (bool, error) {
if addr == "" {
// If we cannot determine the source address, but there are element in either the Allow or the Block list, the only safe thing we can do is to reject
if hook.AllowAddresses != nil && len(hook.AllowAddresses) > 0 {
return false, fmt.Errorf("no source address")
}
if hook.BlockedAddresses != nil && len(hook.BlockedAddresses) > 0 {
return false, fmt.Errorf("no source address")
}
}
addr = cidr(addr)
if hook.AllowAddresses != nil && len(hook.AllowAddresses) > 0 {
// If AllowAddresses is defined and not empty, the given addr must be in the AllowAddresses list
found, err := isAddressInList(addr, hook.AllowAddresses)
if err != nil {
return false, err
}
// If not present in the list, block the request. Otherwise we still need to pass the BlockedAddresses check
if !found {
return false, err
}
}
if hook.BlockedAddresses != nil && len(hook.BlockedAddresses) > 0 {
// If BlockedAddresses is defined and not empty, the given addr must not be in the BlockedAddresses list
found, err := isAddressInList(addr, hook.BlockedAddresses)
if err != nil {
return false, err
}
if found {
return false, err
}
}
return true, nil
}
07070100000008000081A4000000000000000000000001647EF3710000192E000000000000000000000000000000000000002000000000weblug-0.4/cmd/weblug/weblug.go/*
* weblug main program
*/
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
)
var cf Config
type Handler func(http.ResponseWriter, *http.Request)
func usage() {
fmt.Println("weblug is a webhook receiver")
fmt.Printf("Usage: %s [OPTIONS] YAML1[,YAML2...]\n\n", os.Args[0])
fmt.Println("OPTIONS")
fmt.Println(" -h, --help Print this help message")
fmt.Println()
fmt.Println("The program loads the given yaml files for webhook definitions")
}
// awaits SIGINT or SIGTERM
func awaitTerminationSignal() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigs
fmt.Println(sig)
os.Exit(1)
}()
}
// Perform sanity check on hooks
func sanityCheckHooks(hooks []Hook) error {
// Check UID and GID settings. When hooks have their own UID and GID settings, we need the main program to run as root (required for setgid/setuid)
uid := cf.Settings.UID
if uid == 0 {
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 {
usage()
}
for _, arg := range os.Args[1:] {
if arg == "" {
continue
} else if arg == "-h" || arg == "--help" {
usage()
os.Exit(0)
} else {
if err := cf.LoadYAML((arg)); err != nil {
fmt.Fprintf(os.Stderr, "yaml error: %s\n", err)
os.Exit(1)
}
}
}
if len(cf.Hooks) == 0 {
fmt.Fprintf(os.Stderr, "error: no webhooks defined\n")
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)
}
// Drop privileges?
if cf.Settings.GID != 0 {
if err := syscall.Setgid(cf.Settings.GID); err != nil {
fmt.Fprintf(os.Stderr, "setgid failed: %s\n", err)
os.Exit(1)
}
}
if cf.Settings.UID != 0 {
if err := syscall.Setuid(cf.Settings.UID); err != nil {
fmt.Fprintf(os.Stderr, "setuid failed: %s\n", err)
os.Exit(1)
}
}
// Create default handlers
http.HandleFunc("/health", createHealthHandler())
http.HandleFunc("/health.json", createHealthHandler())
http.HandleFunc("/index", createDefaultHandler())
http.HandleFunc("/index.htm", createDefaultHandler())
http.HandleFunc("/index.html", createDefaultHandler())
http.HandleFunc("/robots.txt", createRobotsHandler())
// Register hooks
for i, hook := range cf.Hooks {
if hook.Route == "" {
fmt.Fprintf(os.Stderr, "Invalid hook %s: No route defined\n", hook.Name)
}
if hook.Concurrency < 1 {
hook.Concurrency = 1
}
log.Printf("Webhook %d: '%s' [%s] \"%s\"\n", i, hook.Name, hook.Route, hook.Command)
http.HandleFunc(hook.Route, createHandler(hook))
}
awaitTerminationSignal()
log.Printf("Launching webserver on %s", cf.Settings.BindAddress)
log.Fatal(http.ListenAndServe(cf.Settings.BindAddress, nil))
}
// create a http handler function from the given hook
func createHandler(hook Hook) Handler {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("GET %s %s", r.RemoteAddr, hook.Name)
// Check if adresses are allowed or blocked
allowed, err := hook.IsAddressAllowed(r.RemoteAddr)
if err != nil {
log.Printf("ERR: Error checking for address permissions for hook \"%s\": %s", hook.Name, err)
w.WriteHeader(500)
fmt.Fprintf(w, "{\"status\":\"fail\",\"reason\":\"server error\"}")
return
}
if !allowed {
log.Printf("Blocked: '%s' for not allowed remote end %s", hook.Name, r.RemoteAddr)
// Return a 403 - Forbidden
w.WriteHeader(403)
fmt.Fprintf(w, "{\"status\":\"blocked\",\"reason\":\"address not allowed\"}")
return
}
// Check for basic auth, if enabled
if hook.HttpBasicAuth.Password != "" {
username, password, ok := r.BasicAuth()
if !ok {
// Return a 403 - Forbidden
w.WriteHeader(401)
fmt.Fprintf(w, "{\"status\":\"unauthorized\",\"message\":\"webhook requires authentication\"}")
return
}
if hook.HttpBasicAuth.Password != password || (hook.HttpBasicAuth.Username != "" && hook.HttpBasicAuth.Username != username) {
// Return a 403 - Forbidden
w.WriteHeader(403)
fmt.Fprintf(w, "{\"status\":\"blocked\",\"reason\":\"not authenticated\"}")
return
}
}
// Check for available slots
if !hook.TryLock() {
log.Printf("ERR: \"%s\" max concurrency reached", hook.Name)
// Return a 503 - Service Unavailable error
w.Header().Add("Content-Type", "application/json")
w.Header().Add("Retry-After", "120") // Suggest to retry after 2 minutes
w.WriteHeader(503)
fmt.Fprintf(w, "{\"status\":\"fail\",\"reason\":\"max concurrency reached\"}")
return
}
if hook.Background { // Execute command in background
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(200)
fmt.Fprintf(w, "{\"status\":\"ok\"}")
go func() {
defer hook.Unlock()
if err := hook.Run(); err != nil {
log.Printf("Hook \"%s\" failed: %s", hook.Name, err)
} else {
log.Printf("Hook \"%s\" completed", hook.Name)
}
}()
} else {
defer hook.Unlock()
if err := hook.Run(); err != nil {
log.Printf("ERR: \"%s\" exec failure: %s", hook.Name, err)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(500)
fmt.Fprintf(w, "{\"status\":\"fail\",\"reason\":\"program error\"}")
} else {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(200)
fmt.Fprintf(w, "{\"status\":\"ok\"}")
}
}
}
}
func createHealthHandler() Handler {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(200)
fmt.Fprintf(w, "{\"status\":\"ok\"}")
}
}
func createDefaultHandler() Handler {
return func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
fmt.Fprintf(w, "weblug - webhook receiver program\nSee https://codeberg.org/grisu48/weblug\n")
}
}
func createRobotsHandler() Handler {
return func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
fmt.Fprintf(w, "User-agent: *\nDisallow: /")
}
}
07070100000009000041ED000000000000000000000001647EF37100000000000000000000000000000000000000000000000F00000000weblug-0.4/doc0707010000000A000081A4000000000000000000000001647EF37100000F90000000000000000000000000000000000000001800000000weblug-0.4/doc/weblug.8." Manpage for weblug
." Write me an email <felix@feldspaten.org> if you find errors and/or typos. Thank you! :-)
.TH weblug 8 "28 May 2023" "1.0" "weblug man page"
.SH NAME
weblug - Simple webhook receiver program
.SH SYNOPSIS
weblug [OPTIONS] YAML1[,YAML2...]
.SH DESCRIPTION
weblug is is a configurable webhook receiver that allows to define custom programs and script to be executed when a webhook is triggered.
The configuration happens via yaml files. weblug supports multiple webhooks, limitations for concurrent web hooks to be executed, background execution and running webhooks as separate user (uid/gid) and basic auth.
The system daemon uses the /etc/weblug.yml file. To enable the daemon, edit /etc/weblug.yml to your needs and then simply start/enable the system service.
.SH OPTIONS
.TP
.B -h|--help
Print help message
.SH CAVEATS
1. weblug should always run behind a http reverse proxy to avoid a whole class of security issues by using the standart go webserver implementation.
2. weblug does not support transport encryption (https). To protect access credentials/tokens, it must run behind a http reverse proxy with configured transport encryption.
3. weblug should not be exposed to the public internet.
4. Custom UID/GIDs for webhook require weblug to run as root.
.SH CONFIGURATION FILES
.TP
weblug needs a configuration file with webhook definitions to run. The program needs at least one configuration file, multiple files are supported.
See the following example configuration file:
.B "---
.br
.B "## Weblug example config
.br
.B "settings:
.br
.B " #bind: "127.0.0.1:2088" # bind address for webserver
.br
.B " bind: ":2088" # bind to all addresses
.br
.B " uid: 0 # run under specified user id
.br
.B " gid: 0 # run under specified group id
.br
.B "# hook definitions. A hook needs to define the HTTP endpoint ("route") and the command
.br
.B "# See the following examples for more possible options.
.br
.B "hooks:
.br
.B " - name: 'hook one'
.br
.B " route: "/webhooks/1"
.br
.B " command: "sleep 5"
.br
.B " background: True # Terminate http request immediately
.br
.B " concurrency: 2 # At most 2 parallel processes are allowed
.br
.B " env: # Define environment variables
.br
.B " KEY1: "VALUE1"
.br
.B " KEY2: "VALUE2"
.br
.br
.br
.br
.B " - name: 'hook two'
.br
.B " route: "/webhooks/2"
.br
.B " command: "bash -c 'sleep 5'"
.br
.B " concurrency: 5 # At most 5 parallel processes are allowed
.br
.br
.br
.B " - name: 'hook 3'
.br
.B " route: "/webhooks/data/3"
.br
.B " command: "bash -c 'echo $UID $GID'"
.br
.B " uid: 100 # Run command as system user id (uid) 100
.br
.B " gid: 200 # Run command with system group id (gid) 200
.br
.B " concurrency: 1 # No concurrency. Returns 500 on parallel requests
.br
.B " output: True # Print program output to console
.br
.br
.br
.B " - name: 'hook 4'
.br
.B " route: "/webhooks/restricted/4"
.br
.br
.B " command: "true"
.br
.B " # Allow only requests from localhost
.br
.B " allowed: ["127.0.0.1/8", "::1/128"]
.br
.br
.br
.B " - name: 'hook 5'
.br
.B " route: "/webhooks/restricted/5"
.br
.B " command: "true"
.br
.B " # Allow everything, except those two subnets
.br
.B " blocked: ["192.168.0.0/16", "10.0.0.0/8"]
.br
.br
.B " - name: 'hook auth'
.br
.B " route: "/webhooks/restricted/auth"
.br
.B " command: "true"
.br
.B " # Require basic auth for this webhook
.br
.B " basic_auth:
.br
.B " # Username is optional. If defined, the following username must match
.br
.B " # If not defined, any user will be accepted
.br
.B " username: 'user'
.br
.B " # Password is obligatory to enable basic_auth. If defined, a request must authenticate with the given password (cleartext)
.br
.B " password: 'password'
.br
0707010000000B000081A4000000000000000000000001647EF3710000003D000000000000000000000000000000000000001200000000weblug-0.4/go.modmodule weblug/m/v2
go 1.18
require gopkg.in/yaml.v2 v2.4.0
0707010000000C000081A4000000000000000000000001647EF37100000168000000000000000000000000000000000000001200000000weblug-0.4/go.sumgopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
0707010000000D000041ED000000000000000000000001647EF37100000000000000000000000000000000000000000000001000000000weblug-0.4/test0707010000000E000081ED000000000000000000000001647EF3710000078B000000000000000000000000000000000000001C00000000weblug-0.4/test/blackbox.sh#!/bin/bash -ex
# 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
# Secret environment variable, which must be removed by env sanitation.
export SECRET1="top5ecret"
rm -f testfile
../weblug test.yaml &
../weblug test_uid.yaml &
sleep 1
## Check touch webhook, which creates "testfile"
echo -e "\n\nChecking 'testfile' webhook ... "
curl --fail 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 --fail 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 --fail http://127.0.0.1:2088/3 &
timeout 10 curl --fail http://127.0.0.1:2088/3 &
! timeout 2 curl --fail 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 --fail http://127.0.0.1:2088/webhooks/uid
curl --fail http://127.0.0.1:2088/webhooks/gid
else
echo "Cannot UID and GID webhook tests, because we're not running as root"
fi
## Check environment variables
timeout 10 curl --fail 'http://127.0.0.1:2088/env'
## Check UID/GID handling
# Ensure weblug is running with UID=65534
pgrep -u 65534 weblug
# Ensure weblug2 (test_uid) is actually running
timeout 10 curl --fail 'http://127.0.0.1:2089/uid'
echo -e "\n\nall good"
0707010000000F000081ED000000000000000000000001647EF371000002FA000000000000000000000000000000000000001900000000weblug-0.4/test/checkenv#!/bin/bash -e
# Script to test for environment variable sanitation
set -o pipefail
if [[ $PUBLIC1 != "one" ]]; then
echo "PUBLIC1 variable not valid"
exit 1
fi
if [[ $PUBLIC2 != "two" ]]; then
echo "PUBLIC2 variable not valid"
exit 1
fi
if env | grep 'SECRET1' >/dev/null; then
echo "SECRET1 variable is set but it should not be"
echo "Environment sanitation failed"
exit 1
fi
# There must never be more than 10 variables set
# Some variables will be set by bash at startup (e.g. PWD), so we are never in a
# pristine environment. However, more than 10 variables indicates something's off
# with the env sanitation
if [[ `env | wc -l` -ge 10 ]]; then
echo "More than 10 env variables detected"
exit 1
fi
echo "all good"07070100000010000081A4000000000000000000000001647EF37100000352000000000000000000000000000000000000001A00000000weblug-0.4/test/test.yaml---
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
- name: 'environment variables'
route: '/env'
command: "bash ./checkenv"
output: True
env:
PUBLIC1: "one"
PUBLIC2: "two"
07070100000011000081A4000000000000000000000001647EF371000000B5000000000000000000000000000000000000001E00000000weblug-0.4/test/test_uid.yaml---
settings:
bind: "127.0.0.1:2089" # bind address for webserver
uid: 65534
gid: 65534
hooks:
- name: 'uid hook'
route: "/uid"
command: "id -u"
output: True07070100000012000081A4000000000000000000000001647EF371000000DA000000000000000000000000000000000000001A00000000weblug-0.4/weblug.service[Unit]
Description=weblug - webhook Service
After=network.target
[Service]
Type=simple
WorkingDirectory=/var/lib/empty
ExecStart=/usr/bin/weblug /etc/weblug.yml
Restart=on-failure
[Install]
WantedBy=multi-user.target07070100000013000081A4000000000000000000000001647EF371000004B2000000000000000000000000000000000000001700000000weblug-0.4/weblug.spec#
# spec file for package weblug
#
Name: weblug
Version: 0.3
Release: 0
Summary: Simple webhook receiver program
License: MIT
URL: https://codeberg.org/grisu48/weblug
Source: weblug-%{version}.tar.gz
Source1: vendor.tar.gz
BuildRequires: golang-packaging
BuildRequires: go1.18
%{go_nostrip}
%{systemd_ordering}
%description
Simple webhook receiver program
%prep
%autosetup -D -a 1
%build
make all GOARGS="-mod vendor -buildmode pie"
%install
install -Dm 755 weblug %{buildroot}/%{_bindir}/weblug
# systemd unit
install -Dpm0644 weblug.service %{buildroot}%{_unitdir}/weblug.service
# configuration file (/etc/weblug.yml)
mkdir -p %{buildroot}%{_sysconfdir}
install -m 600 weblug.yml %{buildroot}%{_sysconfdir}/weblug.yml
# man page
install -Dm 644 doc/weblug.8 %{buildroot}/%{_mandir}/man8/weblug.8
%pre
%service_add_pre weblug.service
%preun
%service_del_preun weblug.service
%post
%service_add_post weblug.service
%postun
%service_del_postun weblug.service
%files
%doc README.md
%license LICENSE
%{_bindir}/weblug
%{_unitdir}/weblug.service
%config %{_sysconfdir}/weblug.yml
%{_mandir}/man8/weblug.8%{?ext_man}
%changelog
07070100000014000081A4000000000000000000000001647EF371000008B0000000000000000000000000000000000000001600000000weblug-0.4/weblug.yml---
## Weblug example config
settings:
#bind: "127.0.0.1:2088" # bind address for webserver
bind: ":2088" # bind to all addresses
# Note: Due to current limitations, weblug needs to run as root when you use custom uid,gid settings per webhook
# This is a known issue, see https://codeberg.org/grisu48/weblug/issues/9
uid: 0 # run under specified user id
gid: 0 # run under specified group id
# hook definitions. A hook needs to define the HTTP endpoint ("route") and the command
# See the following examples for more possible options.
hooks:
- name: 'hook one'
route: "/webhooks/1"
command: "sleep 5"
background: True # Terminate http request immediately
concurrency: 2 # At most 2 parallel processes are allowed
env: # Define environment variables
KEY1: "VALUE1"
KEY2: "VALUE2"
- name: 'hook two'
route: "/webhooks/2"
command: "bash -c 'sleep 5'"
concurrency: 5 # At most 5 parallel processes are allowed
- name: 'hook 3'
route: "/webhooks/data/3"
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
- name: 'hook 4'
route: "/webhooks/restricted/4"
command: "true"
# Allow only requests from localhost
allowed: ["127.0.0.1/8", "::1/128"]
- name: 'hook 5'
route: "/webhooks/restricted/5"
command: "true"
# Allow everything, except those two subnets
blocked: ["192.168.0.0/16", "10.0.0.0/8"]
- name: 'hook auth'
route: "/webhooks/restricted/auth"
command: "true"
# Require basic auth for this webhook
basic_auth:
# Username is optional. If defined, the following username must match
# If not defined, any user will be accepted
username: 'user'
# Password is obligatory to enable basic_auth. If defined, a request must authenticate with the given password (cleartext)
password: 'password'
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!63 blocks