Add allow and blocked to yaml definitions

Allow to define a Allow and Block list of IP addresses per web hook.
This commit is contained in:
Felix Niederwanger 2023-04-22 10:49:48 +02:00
parent a9cda6f310
commit dfb7b3603a
Signed by: phoenix
GPG key ID: 6E77A590E3F6D71C
3 changed files with 107 additions and 11 deletions

View file

@ -2,21 +2,25 @@ 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
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
@ -86,3 +90,69 @@ func (hook *Hook) Run() error {
}
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"]