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:
commit
6a94260c15
4
Makefile
4
Makefile
|
@ -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
|
||||
|
|
|
@ -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
158
cmd/weblug/hook.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
|
|
14
weblug.yaml
14
weblug.yaml
|
@ -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"]
|
||||
|
|
Loading…
Reference in a new issue