Compare commits

...

3 commits

Author SHA1 Message Date
Felix Niederwanger 47319f709f
Introduce tls support
Introduce TLS support for weblug. TLS can be enabled in the config file.
Multiple certificates are supported.
2024-02-24 11:25:47 +01:00
Felix Niederwanger f87a4d7006 Merge pull request 'Add vet' (#21) from vet into main
Reviewed-on: https://codeberg.org/grisu48/weblug/pulls/21
2024-02-08 17:24:27 +00:00
Felix Niederwanger 748438eb3b
Add vet
Adds rule for running go vet.
2024-02-08 18:23:24 +01:00
6 changed files with 119 additions and 16 deletions

4
.gitignore vendored
View file

@ -25,3 +25,7 @@ vendor/
# Go workspace file
go.work
# Possible key and cert files
*.key
*.pem
*.cert

View file

@ -30,3 +30,11 @@ tasks:
- test -f weblug
cmds:
- sudo bash -c "cd test && ./blackbox.sh"
vet:
cmds:
- go vet ./...
certs:
cmds:
- openssl genrsa -out weblug.key 2048
- openssl req -new -x509 -sha256 -key weblug.key -subj "/C=XX/ST=None/L=Nirwana/O=Adeptus Mechanicus/OU=Cult Mechanicus/CN=example1.local" -addext "subjectAltName = DNS:example1.local" -out weblug1.pem -days 365
- openssl req -new -x509 -sha256 -key weblug.key -subj "/C=XX/ST=None/L=Nirwana/O=Adeptus Mechanicus/OU=Cult Mechanicus/CN=example1.local" -addext "subjectAltName = DNS:example1.local" -out weblug2.pem -days 365

View file

@ -13,12 +13,19 @@ type Config struct {
}
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
ReadTimeout int `yaml:"readtimeout"` // Timeout for reading the whole request
WriteTimeout int `yaml:"writetimeout"` // Timeout for writing the whole response
MaxHeaderBytes int `yaml:"maxheadersize"` // Maximum size of the receive body
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
ReadTimeout int `yaml:"readtimeout"` // Timeout for reading the whole request
WriteTimeout int `yaml:"writetimeout"` // Timeout for writing the whole response
MaxHeaderBytes int `yaml:"maxheadersize"` // Maximum size of the receive body
TLS TLSSettings `yaml:"tls"`
}
type TLSSettings struct {
Enabled bool `yaml:"enabled"`
Keyfile string `yaml:"keyfile"`
Certificates []string `yaml:"certificates"`
}
func (cf *Config) SetDefaults() {

View file

@ -12,6 +12,7 @@ import (
type Hook struct {
Name string `yaml:"name"` // name of the hook
Route string `yaml:"route"` // http route
Hosts []string `yaml:"hosts"` // allowed remote hosts
Command string `yaml:"command"` // Actual command to execute
Background bool `yaml:"background"` // Run in background
Concurrency int `yaml:"concurrency"` // Number of allowed concurrent runs

View file

@ -4,6 +4,7 @@
package main
import (
"crypto/tls"
"fmt"
"log"
"net/http"
@ -120,15 +121,62 @@ func main() {
awaitTerminationSignal()
log.Printf("Launching webserver on %s", cf.Settings.BindAddress)
server := &http.Server{
Addr: cf.Settings.BindAddress,
ReadTimeout: time.Duration(cf.Settings.ReadTimeout) * time.Second,
WriteTimeout: time.Duration(cf.Settings.WriteTimeout) * time.Second,
MaxHeaderBytes: cf.Settings.MaxHeaderBytes,
if cf.Settings.TLS.Enabled {
// Note: Sanity check happens at program startup, here we assume all settings are good
log.Printf("Launching tls webserver on %s", cf.Settings.BindAddress)
server := &http.Server{
Addr: cf.Settings.BindAddress,
ReadTimeout: time.Duration(cf.Settings.ReadTimeout) * time.Second,
WriteTimeout: time.Duration(cf.Settings.WriteTimeout) * time.Second,
MaxHeaderBytes: cf.Settings.MaxHeaderBytes,
}
tlsConfig := &tls.Config{}
// Create self-signed certificate, when no keyfile and no certificates are present
if cf.Settings.TLS.Keyfile == "" && len(cf.Settings.TLS.Certificates) == 0 {
// TODO
fmt.Fprintf(os.Stderr, "error: creating self-signed certificates is not yet implemented")
os.Exit(1)
} else {
var err error
// Ensure keyfile AND certificates are present.
if cf.Settings.TLS.Keyfile == "" {
fmt.Fprintf(os.Stderr, "no keyfile defined\n")
os.Exit(1)
}
if len(cf.Settings.TLS.Certificates) == 0 {
fmt.Fprintf(os.Stderr, "no certificates defined\n")
os.Exit(1)
}
// Load certificates
tlsConfig.Certificates = make([]tls.Certificate, len(cf.Settings.TLS.Certificates))
for i, cert := range cf.Settings.TLS.Certificates {
tlsConfig.Certificates[i], err = tls.LoadX509KeyPair(cert, cf.Settings.TLS.Keyfile)
if err != nil {
fmt.Fprintf(os.Stderr, "error: certificate '%s' invalid: %s\n", cert, err)
os.Exit(1)
}
}
}
listener, err := tls.Listen("tcp", cf.Settings.BindAddress, tlsConfig)
if err != nil {
fmt.Fprintf(os.Stderr, "error: cannot listen on '%s': %s\n", cf.Settings.BindAddress, err)
os.Exit(1)
}
err = server.Serve(listener)
log.Fatal(err)
} else {
log.Printf("Launching webserver on %s", cf.Settings.BindAddress)
server := &http.Server{
Addr: cf.Settings.BindAddress,
ReadTimeout: time.Duration(cf.Settings.ReadTimeout) * time.Second,
WriteTimeout: time.Duration(cf.Settings.WriteTimeout) * time.Second,
MaxHeaderBytes: cf.Settings.MaxHeaderBytes,
}
err := server.ListenAndServe()
log.Fatal(err)
}
err := server.ListenAndServe()
log.Fatal(err)
log.Printf("unexpected end of main loop")
os.Exit(1)
}
@ -137,6 +185,23 @@ func createHandler(hook Hook) Handler {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("GET %s %s", r.RemoteAddr, hook.Name)
// Check if this hook is remote-host limited
if len(hook.Hosts) > 0 {
allowed := false
for _, host := range hook.Hosts {
if r.RemoteAddr == host {
allowed = true
break
}
}
if !allowed {
// Hook doesn't exist for this host.
respondNotFound(w, r)
return
}
}
// Check if adresses are allowed or blocked
allowed, err := hook.IsAddressAllowed(r.RemoteAddr)
if err != nil {
@ -228,8 +293,7 @@ func createDefaultHandler() Handler {
w.WriteHeader(200)
fmt.Fprintf(w, "<!DOCTYPE html><html><head><title>weblug</title></head>\n<body><p><a href=\"https://codeberg.org/grisu48/weblug\">weblug</a> - webhook receiver program</p>\n</body></html>")
} else {
w.WriteHeader(404)
fmt.Fprintf(w, "not found\n")
respondNotFound(w, r)
}
}
}
@ -240,3 +304,9 @@ func createRobotsHandler() Handler {
fmt.Fprintf(w, "User-agent: *\nDisallow: /")
}
}
func respondNotFound(w http.ResponseWriter, r *http.Request) error {
w.WriteHeader(404)
_, err := fmt.Fprintf(w, "not found\n")
return err
}

View file

@ -11,12 +11,25 @@ settings:
readtimeout: 10 # if set, maximum number of seconds to receive the full request
writetimeout: 10 # if set, maximum number of seconds to send the full response
maxheadersize: 4096 # maximum header size
# Enable TLS here here
tls:
enabled: true
keyfile: 'weblug.key'
certificates:
- weblug1.pem
- weblug2.pem
# 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"
# if hosts is present, limit requests to this remote hosts
# Currently multiplexing the same routes to different hosts does not work.
hosts:
- example1.local
command: "sleep 5"
background: True # Terminate http request immediately
concurrency: 2 # At most 2 parallel processes are allowed