Skip to content

Commit

Permalink
refactor(config): small config refactoring
Browse files Browse the repository at this point in the history
- split config structure
- improve config logic
- improve test lib and fix typos
  • Loading branch information
ncarlier authored Mar 4, 2024
1 parent 53f1028 commit 39ab72b
Show file tree
Hide file tree
Showing 21 changed files with 239 additions and 145 deletions.
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ All configuration variables are described in [etc/default/webhookd.env](./etc/de
Webhooks are simple scripts within a directory structure.

By default inside the `./scripts` directory.
You can change the default directory using the `WHD_SCRIPTS` environment variable or `-script` parameter.
You can change the default directory using the `WHD_HOOK_SCRIPTS` environment variable or `-hook-scripts` parameter.

*Example:*

Expand All @@ -89,7 +89,7 @@ The directory structure define the webhook URL.

You can omit the script extension. If you do, webhookd will search by default for a `.sh` file.
You can change the default extension using the `WHD_HOOK_DEFAULT_EXT` environment variable or `-hook-default-ext` parameter.
If the script exists, the output the will be streamed to the HTTP response.
If the script exists, the output will be streamed to the HTTP response.

The streaming technology depends on the HTTP request:

Expand Down Expand Up @@ -218,7 +218,7 @@ $ # Retrieve logs afterwards
$ curl http://localhost:8080/echo/2
```

If needed, you can also redirect hook logs to the server output (configured by the `WHD_LOG_HOOK_OUTPUT` environment variable).
If needed, you can also redirect hook logs to the server output (configured by the `WHD_LOG_MODULES=hook` environment variable).

### Post hook notifications

Expand Down Expand Up @@ -327,12 +327,12 @@ Webhookd supports 2 signature methods:
- [HTTP Signatures](https://www.ietf.org/archive/id/draft-cavage-http-signatures-12.txt)
- [Ed25519 Signature](https://ed25519.cr.yp.to/) (used by [Discord](https://discord.com/developers/docs/interactions/receiving-and-responding#security-and-authorization))

To activate request signature verification, you have to configure the trust store:
To activate request signature verification, you have to configure the truststore:

```bash
$ export WHD_TRUST_STORE_FILE=/etc/webhookd/pubkey.pem
$ export WHD_TRUSTSTORE_FILE=/etc/webhookd/pubkey.pem
$ # or
$ webhookd --trust-store-file /etc/webhookd/pubkey.pem
$ webhookd --truststore-file /etc/webhookd/pubkey.pem
```

Public key is stored in PEM format.
Expand Down Expand Up @@ -361,9 +361,9 @@ You can find a small HTTP client in the ["tooling" directory](./tooling/httpsig/
You can activate TLS to secure communications:

```bash
$ export WHD_TLS=true
$ export WHD_TLS_ENABLED=true
$ # or
$ webhookd --tls
$ webhookd --tls-enabled
```

By default webhookd is expecting a certificate and key file (`./server.pem` and `./server.key`).
Expand All @@ -373,10 +373,10 @@ Webhookd also support [ACME](https://ietf-wg-acme.github.io/acme/) protocol.
You can activate ACME by setting a fully qualified domain name:

```bash
$ export WHD_TLS=true
$ export WHD_TLS_ENABLED=true
$ export WHD_TLS_DOMAIN=hook.example.com
$ # or
$ webhookd --tls --tls-domain=hook.example.com
$ webhookd --tls-enabled --tls-domain=hook.example.com
```

**Note:**
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ services:
ports:
- "8080:8080"
environment:
- WHD_SCRIPTS=/scripts
- WHD_HOOK_SCRIPTS=/scripts
volumes:
- ./scripts:/scripts
8 changes: 4 additions & 4 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ if [ ! -z "$WHD_SCRIPTS_GIT_URL" ]
then
[ ! -f "$WHD_SCRIPTS_GIT_KEY" ] && die "Git clone key not found."

export WHD_SCRIPTS=${WHD_SCRIPTS:-/opt/scripts-git}
export WHD_HOOK_SCRIPTS=${WHD_HOOK_SCRIPTS:-/opt/scripts-git}
export GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"

mkdir -p $WHD_SCRIPTS
mkdir -p $WHD_HOOK_SCRIPTS

echo "Cloning $WHD_SCRIPTS_GIT_URL into $WHD_SCRIPTS ..."
ssh-agent sh -c 'ssh-add ${WHD_SCRIPTS_GIT_KEY}; git clone --depth 1 --single-branch ${WHD_SCRIPTS_GIT_URL} ${WHD_SCRIPTS}'
echo "Cloning $WHD_SCRIPTS_GIT_URL into $WHD_HOOK_SCRIPTS ..."
ssh-agent sh -c 'ssh-add ${WHD_SCRIPTS_GIT_KEY}; git clone --depth 1 --single-branch ${WHD_SCRIPTS_GIT_URL} ${WHD_HOOK_SCRIPTS}'
[ $? != 0 ] && die "Unable to clone repository"
fi

Expand Down
68 changes: 32 additions & 36 deletions etc/default/webhookd.env
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,37 @@
# Webhookd configuration
###

# Hook execution logs location, default is OS temporary directory
#WHD_HOOK_LOG_DIR="/tmp"

# Maximum hook execution time in second, default is 10
#WHD_HOOK_TIMEOUT=10

# HTTP listen address, default is ":8080"
# Example: `localhost:8080` or `:8080` for all interfaces
#WHD_LISTEN_ADDR=":8080"

# Log level (debug, info, warn or error), default is "info"
#WHD_LOG_LEVEL=info

# Log format (text or json), default is "text"
#WHD_LOG_FORMAT=text
# Logging modules to activate (http, hook)
# - `http`: HTTP access logs
# - `hook`: Hook execution logs
# Example: `http` or `http,hook`
#WHD_LOG_MODULES=

# Log HTTP request, default is false
#WHD_LOG_HTTP_REQUEST=false

# Log hook execution output, default is false
#WHD_LOG_HOOK_OUTPUT=false

# Default extension for hook scripts, default is "sh"
#WHD_HOOK_DEFAULT_EXT=sh
# Maximum hook execution time in second, default is 10
#WHD_HOOK_TIMEOUT=10
# Scripts location, default is "scripts"
#WHD_HOOK_SCRIPTS="scripts"
# Hook execution logs location, default is OS temporary directory
#WHD_HOOK_LOG_DIR="/tmp"
# Number of workers to start, default is 2
#WHD_NB_WORKERS=2
#WHD_HOOK_WORKERS=2

# Static file directory to serve on /static path, disabled by default
# Example: `./var/www`
#WHD_STATIC_DIR=
# Path to serve static file directory, default is "/static"
#WHD_STATIC_PATH=/static

# Notification URI, disabled by default
# Example: `http://requestb.in/v9b229v9` or `mailto:foo@bar.com?smtp=smtp-relay-localnet:25`
Expand All @@ -34,37 +41,26 @@
# Password file for HTTP basic authentication, default is ".htpasswd"
#WHD_PASSWD_FILE=".htpasswd"

# Scripts location, default is "scripts"
#WHD_SCRIPTS="scripts"

# GIT repository that contains scripts
# Note: this is only used by the Docker image or by using the Docker entrypoint script
# Example: `git@github.com:ncarlier/webhookd.git`
#WHD_SCRIPTS_GIT_URL=

# GIT SSH private key used to clone the repository
# Note: this is only used by the Docker image or by using the Docker entrypoint script
# Example: `/etc/webhookd/github_deploy_key.pem`
#WHD_SCRIPTS_GIT_KEY=

# Static file directory to serve on /static path, disabled by default
# Example: `./var/www`
#WHD_STATIC_DIR=

# Trust store URI, disabled by default
# Truststore URI, disabled by default
# Enable HTTP signature verification if set.
# Example: `/etc/webhookd/pubkey.pem`
#WHD_TRUST_STORE_FILE=
#WHD_TRUSTSTORE_FILE=

# Activate TLS, default is false
#WHD_TLS=false

#WHD_TLS_ENABLED=false
# TLS key file, default is "./server.key"
#WHD_TLS_KEY_FILE="./server.key"

# TLS certificate file, default is "./server.crt"
#WHD_TLS_CERT_FILE="./server.pem"

# TLS domain name used by ACME, key and cert files are ignored if set
# Example: `hook.example.org`
#WHD_TLS_DOMAIN=

# GIT repository that contains scripts
# Note: this is only used by the Docker image or by using the Docker entrypoint script
# Example: `git@github.com:ncarlier/webhookd.git`
#WHD_SCRIPTS_GIT_URL=
# GIT SSH private key used to clone the repository
# Note: this is only used by the Docker image or by using the Docker entrypoint script
# Example: `/etc/webhookd/github_deploy_key.pem`
#WHD_SCRIPTS_GIT_KEY=
23 changes: 14 additions & 9 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"
"os"
"os/signal"
"slices"
"syscall"
"time"

Expand All @@ -22,9 +23,11 @@ import (
"github.com/ncarlier/webhookd/pkg/worker"
)

const envPrefix = "WHD"

func main() {
conf := &config.Config{}
configflag.Bind(conf, "WHD")
configflag.Bind(conf, envPrefix)

flag.Parse()

Expand All @@ -33,30 +36,32 @@ func main() {
os.Exit(0)
}

if conf.HookLogDir == "" {
conf.HookLogDir = os.TempDir()
if conf.Hook.LogDir == "" {
conf.Hook.LogDir = os.TempDir()
}

if err := conf.Validate(); err != nil {
log.Fatal("invalid configuration:", err)
}

logger.Configure(conf.LogFormat, conf.LogLevel)
logger.HookOutputEnabled = conf.LogHookOutput
logger.RequestOutputEnabled = conf.LogHTTPRequest
logger.Configure(conf.Log.Format, conf.Log.Level)
logger.HookOutputEnabled = slices.Contains(conf.Log.Modules, "hook")
logger.RequestOutputEnabled = slices.Contains(conf.Log.Modules, "http")

conf.ManageDeprecatedFlags(envPrefix)

slog.Debug("starting webhookd server...")

srv := server.NewServer(conf)

// Configure notification
if err := notification.Init(conf.NotificationURI); err != nil {
if err := notification.Init(conf.Notification.URI); err != nil {
slog.Error("unable to create notification channel", "err", err)
}

// Start the dispatcher.
slog.Debug("starting the dispatcher...", "workers", conf.NbWorkers)
worker.StartDispatcher(conf.NbWorkers)
slog.Debug("starting the dispatcher...", "workers", conf.Hook.Workers)
worker.StartDispatcher(conf.Hook.Workers)

done := make(chan bool)
quit := make(chan os.Signal, 1)
Expand Down
30 changes: 17 additions & 13 deletions pkg/api/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ func atoiFallback(str string, fallback int) int {

// index is the main handler of the API.
func index(conf *config.Config) http.Handler {
defaultTimeout = conf.HookTimeout
defaultExt = conf.HookDefaultExt
scriptDir = conf.ScriptDir
outputDir = conf.HookLogDir
defaultTimeout = conf.Hook.Timeout
defaultExt = conf.Hook.DefaultExt
scriptDir = conf.Hook.ScriptsDir
outputDir = conf.Hook.LogDir
return http.HandlerFunc(webhookHandler)
}

Expand Down Expand Up @@ -65,14 +65,16 @@ func triggerWebhook(w http.ResponseWriter, r *http.Request) {
}
script, err := hook.ResolveScript(scriptDir, hookName, defaultExt)
if err != nil {
slog.Error("hooke not found", "err", err.Error())
http.Error(w, "hook not found", http.StatusNotFound)
msg := "hook not found"
slog.Error(msg, "err", err.Error())
http.Error(w, msg, http.StatusNotFound)
return
}

if err = r.ParseForm(); err != nil {
slog.Error("error reading from-data", "err", err)
http.Error(w, "unable to parse request form", http.StatusBadRequest)
msg := "unable to parse form-data"
slog.Error(msg, "err", err)
http.Error(w, msg, http.StatusBadRequest)
return
}

Expand All @@ -84,8 +86,9 @@ func triggerWebhook(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(mediatype, "text/") || mediatype == "application/json" {
body, err = io.ReadAll(r.Body)
if err != nil {
slog.Error("error reading body", "err", err)
http.Error(w, "unable to read request body", http.StatusBadRequest)
msg := "unable to read request body"
slog.Error(msg, "err", err)
http.Error(w, msg, http.StatusBadRequest)
return
}
}
Expand All @@ -106,8 +109,9 @@ func triggerWebhook(w http.ResponseWriter, r *http.Request) {
OutputDir: outputDir,
})
if err != nil {
slog.Error("error creating hook job", "err", err)
http.Error(w, "unable to create hook job", http.StatusInternalServerError)
msg := "unable to create hook execution job"
slog.Error(msg, "err", err)
http.Error(w, msg, http.StatusInternalServerError)
return
}

Expand Down Expand Up @@ -163,7 +167,7 @@ func getWebhookLog(w http.ResponseWriter, r *http.Request) {
return
}
if logFile == nil {
http.Error(w, "job not found", http.StatusNotFound)
http.Error(w, "hook execution log not found", http.StatusNotFound)
return
}
defer logFile.Close()
Expand Down
8 changes: 4 additions & 4 deletions pkg/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ var commonMiddlewares = middleware.Middlewares{

func buildMiddlewares(conf *config.Config) middleware.Middlewares {
var middlewares = commonMiddlewares
if conf.TLS {
if conf.TLS.Enabled {
middlewares = middlewares.UseAfter(middleware.HSTS)
}

// Load trust store...
ts, err := truststore.New(conf.TrustStoreFile)
ts, err := truststore.New(conf.TruststoreFile)
if err != nil {
slog.Warn("unable to load trust store", "filename", conf.TrustStoreFile, "err", err)
slog.Warn("unable to load trust store", "filename", conf.TruststoreFile, "err", err)
}
if ts != nil {
middlewares = middlewares.UseAfter(middleware.Signature(ts))
Expand All @@ -44,7 +44,7 @@ func buildMiddlewares(conf *config.Config) middleware.Middlewares {

func routes(conf *config.Config) Routes {
middlewares := buildMiddlewares(conf)
staticPath := conf.StaticPath + "/"
staticPath := conf.Static.Path + "/"
return Routes{
route(
"/",
Expand Down
4 changes: 2 additions & 2 deletions pkg/api/static.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (

func static(prefix string) HandlerFunc {
return func(conf *config.Config) http.Handler {
if conf.StaticDir != "" {
fs := http.FileServer(http.Dir(conf.StaticDir))
if conf.Static.Dir != "" {
fs := http.FileServer(http.Dir(conf.Static.Dir))
return http.StripPrefix(prefix, fs)
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
8 changes: 4 additions & 4 deletions pkg/api/test/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ func TestQueryParamsToShellVars(t *testing.T) {
"list": []string{"foo", "bar"},
}
values := api.HTTPParamsToShellVars(tc)
assert.ContainsStr(t, "string=foo", values, "")
assert.ContainsStr(t, "list=foo,bar", values, "")
assert.Contains(t, "string=foo", values, "")
assert.Contains(t, "list=foo,bar", values, "")
}

func TestHTTPHeadersToShellVars(t *testing.T) {
Expand All @@ -25,6 +25,6 @@ func TestHTTPHeadersToShellVars(t *testing.T) {
"X-Foo-Bar": []string{"foo", "bar"},
}
values := api.HTTPParamsToShellVars(tc)
assert.ContainsStr(t, "content_type=text/plain", values, "")
assert.ContainsStr(t, "x_foo_bar=foo,bar", values, "")
assert.Contains(t, "content_type=text/plain", values, "")
assert.Contains(t, "x_foo_bar=foo,bar", values, "")
}
Loading

0 comments on commit 39ab72b

Please sign in to comment.