Merge pull request 'Add allow and block' (#2) from allow into main

Reviewed-on: https://codeberg.org/grisu48/weblug/pulls/2
This commit is contained in:
Felix Niederwanger 2023-04-22 08:51:30 +00:00
commit 6a94260c15
5 changed files with 188 additions and 87 deletions

View file

@ -1,10 +1,10 @@
default: all
all: weblug
weblug: cmd/weblug/weblug.go cmd/weblug/config.go
weblug: cmd/weblug/*.go
go build -o weblug $^
static: cmd/weblug/weblug.go cmd/weblug/config.go
static: cmd/weblug/*.go
CGO_ENABLED=0 go build -ldflags="-w -s" -o weblug $^
test: weblug

View file

@ -3,9 +3,6 @@ package main
import (
"fmt"
"io/ioutil"
"os/exec"
"sync/atomic"
"syscall"
"gopkg.in/yaml.v2"
)
@ -19,18 +16,6 @@ type ConfigSettings struct {
BindAddress string `yaml:"bind"` // Bind address for the webserver
}
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
}
func (cf *Config) SetDefaults() {
cf.Settings.BindAddress = ":2088"
}
@ -68,71 +53,3 @@ func (cf *Config) LoadYAML(filename string) error {
}
return cf.Check()
}
// 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)}
}
if hook.Output {
buf, ret := cmd.Output()
fmt.Println(string(buf))
return ret
}
return cmd.Run()
}

158
cmd/weblug/hook.go Normal file
View file

@ -0,0 +1,158 @@
package 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
}
// 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)}
}
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) {
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
}

View file

@ -112,6 +112,22 @@ 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 available slots
if !hook.TryLock() {
log.Printf("ERR: \"%s\" max concurrency reached", hook.Name)

View file

@ -2,8 +2,8 @@
## Weblug example config
settings:
bind: "127.0.0.1:2088" # bind address for webserver
#bind: ":2088" # bind to all addresses
#bind: "127.0.0.1:2088" # bind address for webserver
bind: ":2088" # bind to all addresses
# hook definition. A hook needs to define the HTTP endpoint ("route") and the
# command that will be executed, once this route is executed
@ -24,3 +24,13 @@ hooks:
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"]