Compare commits
11 commits
Author | SHA1 | Date | |
---|---|---|---|
Felix Niederwanger | ef0bc17e67 | ||
d9d8b73b50 | |||
Felix Niederwanger | 6cf62dc77c | ||
f6ebd78c43 | |||
Felix Niederwanger | 888c99e93f | ||
Felix Niederwanger | 2df56b96c9 | ||
Felix Niederwanger | 6887e73b19 | ||
f87a4d7006 | |||
Felix Niederwanger | 748438eb3b | ||
d5f79eae01 | |||
Felix Niederwanger | 9713b0d2c1 |
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -25,3 +25,7 @@ vendor/
|
|||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# Possible key and cert files
|
||||
*.key
|
||||
*.pem
|
||||
*.cert
|
||||
|
|
1
Makefile
1
Makefile
|
@ -10,4 +10,5 @@ static: cmd/weblug/*.go
|
|||
CGO_ENABLED=0 go build -ldflags="-w -s" -o weblug $^
|
||||
|
||||
test: weblug
|
||||
go test ./...
|
||||
sudo bash -c "cd test && ./blackbox.sh"
|
||||
|
|
|
@ -29,4 +29,13 @@ tasks:
|
|||
preconditions:
|
||||
- test -f weblug
|
||||
cmds:
|
||||
- go test ./...
|
||||
- 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
|
||||
|
|
|
@ -1,24 +1,40 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var cf Config
|
||||
|
||||
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
|
||||
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"`
|
||||
MinVersion string `yaml:"minversion"`
|
||||
MaxVersion string `yaml:"maxversion"`
|
||||
Keypairs []TLSKeypairs `yaml:"keypairs"`
|
||||
}
|
||||
|
||||
type TLSKeypairs struct {
|
||||
Keyfile string `yaml:"keyfile"`
|
||||
Certificate string `yaml:"certificate"`
|
||||
}
|
||||
|
||||
func (cf *Config) SetDefaults() {
|
||||
|
@ -63,3 +79,19 @@ func (cf *Config) LoadYAML(filename string) error {
|
|||
}
|
||||
return cf.Check()
|
||||
}
|
||||
|
||||
func ParseTLSVersion(version string) (uint16, error) {
|
||||
if version == "" {
|
||||
return tls.VersionTLS12, nil
|
||||
} else if version == "1.3" {
|
||||
return tls.VersionTLS13, nil
|
||||
} else if version == "1.2" {
|
||||
return tls.VersionTLS12, nil
|
||||
} else if version == "1.1" {
|
||||
return tls.VersionTLS11, nil
|
||||
} else if version == "1.0" {
|
||||
return tls.VersionTLS10, nil
|
||||
} else {
|
||||
return 0, fmt.Errorf("invalid tls version string")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -80,6 +81,10 @@ func cmdSplit(command string) []string {
|
|||
|
||||
// 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 {
|
||||
if hook.Command == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
split := cmdSplit(hook.Command)
|
||||
args := make([]string, 0)
|
||||
if len(split) > 1 {
|
||||
|
|
|
@ -4,17 +4,18 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
var cf Config
|
||||
|
||||
type Handler func(http.ResponseWriter, *http.Request)
|
||||
|
||||
func usage() {
|
||||
|
@ -56,6 +57,20 @@ func sanityCheckHooks(hooks []Hook) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Extract the hostname from an URL
|
||||
func hostname(url string) string {
|
||||
hostname := url
|
||||
i := strings.Index(hostname, "://")
|
||||
if i > 0 {
|
||||
hostname = hostname[i+3:]
|
||||
}
|
||||
i = strings.Index(hostname, ":")
|
||||
if i > 0 {
|
||||
hostname = hostname[:i]
|
||||
}
|
||||
return hostname
|
||||
}
|
||||
|
||||
func main() {
|
||||
cf.SetDefaults()
|
||||
if len(os.Args) < 2 {
|
||||
|
@ -100,38 +115,119 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
server := CreateWebserver(cf)
|
||||
mux := http.NewServeMux()
|
||||
server.Handler = mux
|
||||
if err := RegisterHandlers(cf, mux); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
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)
|
||||
var listener net.Listener
|
||||
var err error
|
||||
if cf.Settings.TLS.Enabled {
|
||||
log.Printf("Launching tls webserver on %s", cf.Settings.BindAddress)
|
||||
listener, err = CreateTLSListener(cf)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
log.Printf("Launching webserver on %s", cf.Settings.BindAddress)
|
||||
listener, err = CreateListener(cf)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
server.Serve(listener)
|
||||
|
||||
// read guard, should never ever ever be called.
|
||||
// If we end up here, the only safe thing we can do is terminate the program
|
||||
panic("unexpected end of main loop")
|
||||
}
|
||||
|
||||
func CreateListener(cf Config) (net.Listener, error) {
|
||||
return net.Listen("tcp", cf.Settings.BindAddress)
|
||||
}
|
||||
|
||||
func CreateTLSListener(cf Config) (net.Listener, error) {
|
||||
var err error
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
if cf.Settings.TLS.MinVersion != "" {
|
||||
tlsConfig.MinVersion, err = ParseTLSVersion(cf.Settings.TLS.MinVersion)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid tls min version")
|
||||
}
|
||||
}
|
||||
if cf.Settings.TLS.MaxVersion != "" {
|
||||
tlsConfig.MaxVersion, err = ParseTLSVersion(cf.Settings.TLS.MaxVersion)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid tls max version")
|
||||
}
|
||||
}
|
||||
if tlsConfig.MinVersion == tls.VersionTLS10 || tlsConfig.MinVersion == tls.VersionTLS11 {
|
||||
fmt.Fprintf(os.Stderr, "warning: using of a deprecated TLS version (< 1.2) is not recommended\n")
|
||||
}
|
||||
|
||||
// Create self-signed certificate, when no keyfile and no certificates are present
|
||||
if len(cf.Settings.TLS.Keypairs) == 0 {
|
||||
// TODO
|
||||
return nil, fmt.Errorf("creating self-signed certificates is not yet supported")
|
||||
} else {
|
||||
// Load key/certificates keypairs
|
||||
tlsConfig.Certificates = make([]tls.Certificate, len(cf.Settings.TLS.Keypairs))
|
||||
for i, keypair := range cf.Settings.TLS.Keypairs {
|
||||
tlsConfig.Certificates[i], err = tls.LoadX509KeyPair(keypair.Certificate, keypair.Keyfile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid tls keypair '%s,%s' - %s", keypair.Certificate, keypair.Keyfile, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(tlsConfig.Certificates) == 1 {
|
||||
log.Printf("Loaded 1 tls certificate")
|
||||
} else {
|
||||
log.Printf("Loaded %d tls certificates", len(tlsConfig.Certificates))
|
||||
}
|
||||
}
|
||||
return tls.Listen("tcp", cf.Settings.BindAddress, tlsConfig)
|
||||
}
|
||||
|
||||
func CreateWebserver(cf Config) *http.Server {
|
||||
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)
|
||||
os.Exit(1)
|
||||
return server
|
||||
}
|
||||
|
||||
func RegisterHandlers(cf Config, mux *http.ServeMux) error {
|
||||
// Register default paths
|
||||
mux.HandleFunc("/", createDefaultHandler())
|
||||
mux.HandleFunc("/health", createHealthHandler())
|
||||
mux.HandleFunc("/health.json", createHealthHandler())
|
||||
mux.HandleFunc("/robots.txt", createRobotsHandler())
|
||||
|
||||
// Register hooks
|
||||
for _, hook := range cf.Hooks {
|
||||
if hook.Route == "" {
|
||||
return fmt.Errorf("no route defined in hook %s", hook.Name)
|
||||
}
|
||||
if hook.Concurrency < 1 {
|
||||
hook.Concurrency = 1
|
||||
}
|
||||
mux.HandleFunc(hook.Route, createHandler(hook))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// create a http handler function from the given hook
|
||||
|
@ -139,6 +235,25 @@ 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
|
||||
// Extract the queried hostname
|
||||
queriedHost := hostname(r.Host)
|
||||
for _, host := range hook.Hosts {
|
||||
if host == queriedHost {
|
||||
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 {
|
||||
|
@ -223,8 +338,15 @@ func createHealthHandler() Handler {
|
|||
|
||||
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")
|
||||
if r.URL.Path == "/" || r.URL.Path == "/index.txt" {
|
||||
w.WriteHeader(200)
|
||||
fmt.Fprintf(w, "weblug - webhook receiver program\nhttps://codeberg.org/grisu48/weblug\n")
|
||||
} else if r.URL.Path == "/index.htm" || r.URL.Path == "/index.html" {
|
||||
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 {
|
||||
respondNotFound(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -234,3 +356,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
|
||||
}
|
||||
|
|
343
cmd/weblug/weblug_test.go
Normal file
343
cmd/weblug/weblug_test.go
Normal file
|
@ -0,0 +1,343 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// Run tests
|
||||
ret := m.Run()
|
||||
os.Exit(ret)
|
||||
}
|
||||
|
||||
// Test the general webserver functionalities
|
||||
func TestWebserver(t *testing.T) {
|
||||
var cf Config
|
||||
|
||||
cf.Settings.BindAddress = "127.0.0.1:2088"
|
||||
cf.Hooks = make([]Hook, 0)
|
||||
cf.Hooks = append(cf.Hooks, Hook{Route: "/test1", Name: "test1", Command: ""})
|
||||
cf.Hooks = append(cf.Hooks, Hook{Route: "/test2", Name: "test2", Command: ""})
|
||||
|
||||
listener, err := CreateListener(cf)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating listener: %s", err)
|
||||
return
|
||||
}
|
||||
server := CreateWebserver(cf)
|
||||
mux := http.NewServeMux()
|
||||
server.Handler = mux
|
||||
if err := RegisterHandlers(cf, mux); err != nil {
|
||||
t.Fatalf("error registering handlers: %s", err)
|
||||
return
|
||||
}
|
||||
go server.Serve(listener)
|
||||
defer func() {
|
||||
if err := server.Shutdown(context.Background()); err != nil {
|
||||
t.Fatalf("error while server shutdown: %s", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
assertStatusCode := func(url string, statusCode int) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
return
|
||||
}
|
||||
if resp.StatusCode != statusCode {
|
||||
t.Fatalf("GET / returns status code %d != %d", resp.StatusCode, statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// Check default sites
|
||||
assertStatusCode(fmt.Sprintf("http://%s/", cf.Settings.BindAddress), http.StatusOK)
|
||||
assertStatusCode(fmt.Sprintf("http://%s/health", cf.Settings.BindAddress), http.StatusOK)
|
||||
assertStatusCode(fmt.Sprintf("http://%s/health.json", cf.Settings.BindAddress), http.StatusOK)
|
||||
assertStatusCode(fmt.Sprintf("http://%s/robots.txt", cf.Settings.BindAddress), http.StatusOK)
|
||||
// Check for a 404 page
|
||||
assertStatusCode(fmt.Sprintf("http://%s/404", cf.Settings.BindAddress), http.StatusNotFound)
|
||||
assertStatusCode(fmt.Sprintf("http://%s/test3", cf.Settings.BindAddress), http.StatusNotFound)
|
||||
// Test registered hooks
|
||||
assertStatusCode(fmt.Sprintf("http://%s/test1", cf.Settings.BindAddress), http.StatusOK)
|
||||
assertStatusCode(fmt.Sprintf("http://%s/test2", cf.Settings.BindAddress), http.StatusOK)
|
||||
}
|
||||
|
||||
// Tests the TLS functions of the webserver
|
||||
func TestTLSWebserver(t *testing.T) {
|
||||
var cf Config
|
||||
|
||||
const TESTPORT = 2089
|
||||
|
||||
// Test keypairs. testkey1 belongs to the "localhost" host and testkey2 belongs to the "localhost" and "example.com" hosts
|
||||
keypairs := make([]TLSKeypairs, 0)
|
||||
keypairs = append(keypairs, TLSKeypairs{Keyfile: "testkey1.pem", Certificate: "testcert1.pem"})
|
||||
keypairs = append(keypairs, TLSKeypairs{Keyfile: "testkey2.pem", Certificate: "testcert2.pem"})
|
||||
|
||||
// Generate test certificates
|
||||
for i, keypair := range keypairs {
|
||||
if fileExists(keypair.Keyfile) {
|
||||
t.Fatalf("test key '%s' already exists", keypair.Keyfile)
|
||||
return
|
||||
}
|
||||
if fileExists(keypair.Certificate) {
|
||||
t.Fatalf("test certificate '%s' already exists", keypair.Certificate)
|
||||
return
|
||||
}
|
||||
|
||||
hostnames := []string{"localhost"}
|
||||
if i == 1 {
|
||||
hostnames = append(hostnames, "example.com")
|
||||
}
|
||||
if err := generateKeypair(keypair.Keyfile, keypair.Certificate, hostnames); err != nil {
|
||||
t.Fatalf("keypair generation failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func(keypair TLSKeypairs) {
|
||||
os.Remove(keypair.Keyfile)
|
||||
os.Remove(keypair.Certificate)
|
||||
}(keypair)
|
||||
}
|
||||
|
||||
cf.Settings.BindAddress = fmt.Sprintf("localhost:%d", TESTPORT)
|
||||
cf.Settings.TLS.Enabled = true
|
||||
cf.Settings.TLS.MinVersion = "1.3"
|
||||
cf.Settings.TLS.MaxVersion = "1.3"
|
||||
cf.Settings.TLS.Keypairs = keypairs
|
||||
cf.Hooks = make([]Hook, 0)
|
||||
cf.Hooks = append(cf.Hooks, Hook{Route: "/test1", Name: "test1", Command: "", Hosts: []string{"localhost"}})
|
||||
cf.Hooks = append(cf.Hooks, Hook{Route: "/test2", Name: "test2", Command: "", Hosts: []string{"localhost", "example.com"}})
|
||||
|
||||
// Setup TLS webserver
|
||||
listener, err := CreateTLSListener(cf)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating tls listener: %s", err)
|
||||
return
|
||||
}
|
||||
server := CreateWebserver(cf)
|
||||
mux := http.NewServeMux()
|
||||
server.Handler = mux
|
||||
if err := RegisterHandlers(cf, mux); err != nil {
|
||||
t.Fatalf("error registering handlers: %s", err)
|
||||
return
|
||||
}
|
||||
go server.Serve(listener)
|
||||
defer func() {
|
||||
if err := server.Shutdown(context.Background()); err != nil {
|
||||
t.Fatalf("error while server shutdown: %s", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// Default page without https should return a 400 error
|
||||
resp, err := http.Get(fmt.Sprintf("http://%s/", cf.Settings.BindAddress))
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
return
|
||||
}
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("GET / returns status code %d for default page (400 expected)", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Check default page with tls certificates
|
||||
certs := make([]tls.Certificate, 0)
|
||||
rootCAs, _ := x509.SystemCertPool()
|
||||
for i, keypair := range keypairs {
|
||||
x509cert, err := readCertificate(keypair.Certificate)
|
||||
if err != nil {
|
||||
t.Fatalf("error loading certificate %d: %s", i, err)
|
||||
return
|
||||
}
|
||||
raw := make([][]byte, 0)
|
||||
raw = append(raw, x509cert.Raw)
|
||||
certs = append(certs, tls.Certificate{Certificate: raw})
|
||||
rootCAs.AddCert(x509cert)
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{}
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
Certificates: certs,
|
||||
RootCAs: rootCAs,
|
||||
},
|
||||
// Mock connections to example.com -> 127.0.0.1
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
if strings.Contains(addr, "example.com") {
|
||||
addr = strings.ReplaceAll(addr, "example.com", "127.0.0.1")
|
||||
}
|
||||
return dialer.DialContext(ctx, network, addr)
|
||||
},
|
||||
}
|
||||
client := http.Client{Transport: transport, Timeout: 15 * time.Second}
|
||||
|
||||
assertStatusCode := func(url string, statusCode int) {
|
||||
resp, err = client.Get(url)
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
return
|
||||
}
|
||||
if resp.StatusCode != statusCode {
|
||||
t.Fatalf("GET / returns status code %d != %d", resp.StatusCode, statusCode)
|
||||
}
|
||||
}
|
||||
fetchBody := func(url string) (string, error) {
|
||||
resp, err = client.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
return string(body), err
|
||||
}
|
||||
|
||||
// Check default page and test hooks
|
||||
assertStatusCode(fmt.Sprintf("https://%s/", cf.Settings.BindAddress), http.StatusOK)
|
||||
assertStatusCode(fmt.Sprintf("https://%s/test1", cf.Settings.BindAddress), http.StatusOK)
|
||||
assertStatusCode(fmt.Sprintf("https://%s/test2", cf.Settings.BindAddress), http.StatusOK)
|
||||
assertStatusCode(fmt.Sprintf("https://%s/test404", cf.Settings.BindAddress), http.StatusNotFound)
|
||||
|
||||
// Check if connection via TLS 1.2 is not accepted (we're enforcing TLS >= 1.3)
|
||||
transport.TLSClientConfig.MinVersion = tls.VersionTLS12
|
||||
transport.TLSClientConfig.MaxVersion = tls.VersionTLS12
|
||||
resp, err = client.Get(fmt.Sprintf("https://%s/", cf.Settings.BindAddress))
|
||||
if err == nil {
|
||||
t.Fatal("tls 1.2 connection possible where it should be unsupported", err)
|
||||
return
|
||||
} else {
|
||||
// TODO: Matching by string might be flanky.
|
||||
if !strings.Contains(err.Error(), "tls: protocol version not supported") {
|
||||
t.Fatalf("%s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
transport.TLSClientConfig.MaxVersion = tls.VersionTLS13
|
||||
|
||||
// Check if example.com resolves (second certificate)
|
||||
assertStatusCode(fmt.Sprintf("https://example.com:%d/", TESTPORT), http.StatusOK)
|
||||
// Only /test2 should be reachable via example.com
|
||||
assertStatusCode(fmt.Sprintf("https://example.com:%d/test1", TESTPORT), http.StatusNotFound)
|
||||
assertStatusCode(fmt.Sprintf("https://example.com:%d/test2", TESTPORT), http.StatusOK)
|
||||
|
||||
// Assert, that the host 404 page is the same as the 404 page for a route that doesn't exist.
|
||||
// This check is needed, because we pretend a path to not exist, if `hosts` is configured and
|
||||
// we don't want to give attackers the possibility to distinguish between the two 404 errors
|
||||
if body1, err := fetchBody(fmt.Sprintf("https://%s/test404", cf.Settings.BindAddress)); err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
return
|
||||
} else if body2, err := fetchBody(fmt.Sprintf("https://example.com:%d/test1", TESTPORT)); err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
return
|
||||
} else {
|
||||
if body1 != body2 {
|
||||
t.Fatal("404 bodies differ between default 404 page and host-not-matched route", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateKeypair(keyfile string, certfile string, hostnames []string) error {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write key to file
|
||||
var buffer []byte = x509.MarshalPKCS1PrivateKey(key)
|
||||
block := &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: buffer,
|
||||
}
|
||||
file, err := os.Create(keyfile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
if err := file.Chmod(os.FileMode(0400)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = pem.Encode(file, block); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate certificate
|
||||
notBefore := time.Now()
|
||||
notAfter := notBefore.Add(365 * 24 * 10 * time.Hour)
|
||||
|
||||
//Create certificate templet
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(0),
|
||||
Subject: pkix.Name{CommonName: hostnames[0]},
|
||||
SignatureAlgorithm: x509.SHA256WithRSA,
|
||||
DNSNames: hostnames,
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
BasicConstraintsValid: true,
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement | x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||
}
|
||||
//Create certificate using templet
|
||||
cert, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
block = &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert,
|
||||
}
|
||||
|
||||
file, err = os.Create(certfile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
if err := file.Chmod(os.FileMode(0644)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = pem.Encode(file, block); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readCertificate(certfile string) (*x509.Certificate, error) {
|
||||
buffer, err := os.ReadFile(certfile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p, _ := pem.Decode(buffer)
|
||||
if p == nil {
|
||||
return nil, fmt.Errorf("invalid pem file")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(p.Bytes)
|
||||
return cert, err
|
||||
|
||||
}
|
||||
|
||||
func fileExists(filename string) bool {
|
||||
st, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !st.IsDir()
|
||||
}
|
33
doc/weblug.8
33
doc/weblug.8
|
@ -50,6 +50,29 @@ See the following example configuration file:
|
|||
.br
|
||||
.B " gid: 0 # run under specified group id
|
||||
.br
|
||||
.B " # Enable TLS here here
|
||||
.br
|
||||
.B " tls:
|
||||
.br
|
||||
.B " enabled: true
|
||||
.br
|
||||
.B " # Minimum and maximum required TLS version. By default TLS1.2 is the minimum
|
||||
.br
|
||||
.B " minversion: '1.2'
|
||||
.br
|
||||
.B " maxversion: ''
|
||||
.br
|
||||
.B " keypairs:
|
||||
.br
|
||||
.B " - keyfile: 'weblug.key'
|
||||
.br
|
||||
.B " certificate: 'weblug1.pem'
|
||||
.br
|
||||
.B " - keyfile: 'weblug.key'
|
||||
.br
|
||||
.B " certificate: 'weblug2.pem'
|
||||
.br
|
||||
.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.
|
||||
|
@ -60,6 +83,16 @@ See the following example configuration file:
|
|||
.br
|
||||
.B " route: "/webhooks/1"
|
||||
.br
|
||||
.B " # if hosts is present, then limit the incoming requests to the given remote host(s)
|
||||
.br
|
||||
.B " # Currently multiplexing the same route to different hosts does not work
|
||||
.br
|
||||
.B " hosts:
|
||||
.br
|
||||
.B " - example1.local
|
||||
.br
|
||||
.B " - example2.local
|
||||
.br
|
||||
.B " command: "sleep 5"
|
||||
.br
|
||||
.B " background: True # Terminate http request immediately
|
||||
|
|
18
weblug.yml
18
weblug.yml
|
@ -11,12 +11,30 @@ 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
|
||||
# Minimum and maximum required TLS version. By default TLS1.2 is the minimum
|
||||
minversion: '1.2'
|
||||
maxversion: ''
|
||||
keypairs:
|
||||
- keyfile: 'weblug.key'
|
||||
certificate: 'weblug1.pem'
|
||||
- keyfile: 'weblug.key'
|
||||
certificate: '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, then limit the incoming requests to the given remote host(s)
|
||||
# Currently multiplexing the same route to different hosts does not work
|
||||
hosts:
|
||||
- example1.local
|
||||
- example2.local
|
||||
|
||||
command: "sleep 5"
|
||||
background: True # Terminate http request immediately
|
||||
concurrency: 2 # At most 2 parallel processes are allowed
|
||||
|
|
Loading…
Reference in a new issue