feat(): support application/x-www-form-urlencoded

This commit is contained in:
Nicolas Carlier 2022-05-09 23:17:49 +02:00
parent 213a1c4a29
commit 173ba6c347
4 changed files with 39 additions and 23 deletions

View File

@ -39,14 +39,14 @@ $ docker run -d --name=webhookd \
webhookd --scripts=/var/opt/webhookd/scripts webhookd --scripts=/var/opt/webhookd/scripts
``` ```
> Note that this image extends `docker:dind` Docker image. > Note: This image extends `docker:dind` Docker image.
> Therefore you are able to interact with a Docker daemon with yours shell scripts. > Therefore you are able to interact with a Docker daemon with yours shell scripts.
**Or** use APT: **Or** use APT:
Finally, it is possible to install Webhookd using the Debian packaging system through this [custom repository](https://packages.azlux.fr/). Finally, it is possible to install Webhookd using the Debian packaging system through this [custom repository](https://packages.azlux.fr/).
> Note that custom configuration variables can be set into `/etc/webhookd.env` file. > Note: Custom configuration variables can be set into `/etc/webhookd.env` file.
> Sytemd service is already set and enable, you just have to start it with `systemctl start webhookd`. > Sytemd service is already set and enable, you just have to start it with `systemctl start webhookd`.
## Configuration ## Configuration
@ -78,7 +78,7 @@ You can override the default using the `WHD_SCRIPTS` environment variable or `-s
|--> ... |--> ...
``` ```
Note that Webhookd is able to run any type of file in this directory as long as the file is executable. > Note: Webhookd is able to run any type of file in this directory as long as the file is executable.
For example, you can execute a Node.js file if you give execution rights to the file and add the appropriate `#!` header (in this case: `#!/usr/bin/env node`). For example, you can execute a Node.js file if you give execution rights to the file and add the appropriate `#!` header (in this case: `#!/usr/bin/env node`).
You can find sample scripts in the [example folder](./scripts/examples). You can find sample scripts in the [example folder](./scripts/examples).
@ -139,15 +139,17 @@ data: bar bar bar
### Webhook parameters ### Webhook parameters
You have several way to provide parameters to your webhook script: You have several ways to provide parameters to your webhook script:
- URL query parameters and HTTP headers are converted into environment variables. - URL request parameters are converted to script variables
Variable names follows "snakecase" naming convention. - HTTP headers are converted to script variables
Therefore the name can be altered. - Request body (depending the Media Type):
- `application/x-www-form-urlencoded`: keys and values are converted to script variables
- `text/*` or `application/json`: payload is transmit to the script as first parameter.
*ex: `CONTENT-TYPE` will become `content_type`.* > Note: Variable name follows "snakecase" naming convention.
Therefore the name can be altered.
- When using `POST`, body content (text/plain or application/json) is transmit to the script as parameter. *ex: `CONTENT-TYPE` will become `content_type`.*
*Example:* *Example:*
@ -226,7 +228,7 @@ $ # or
$ webhookd --notification-uri=http://requestb.in/v9b229v9 $ webhookd --notification-uri=http://requestb.in/v9b229v9
``` ```
Note that only the output of the script prefixed by "notify:" is sent to the notification channel. > Note: Only the output of the script prefixed by "notify:" is sent to the notification channel.
If the output does not contain a prefixed line, no notification will be sent. If the output does not contain a prefixed line, no notification will be sent.
**Example:** **Example:**
@ -261,7 +263,7 @@ The following JSON payload is POST to the target URL:
} }
``` ```
Note that because the payload have a `text` attribute, you can use a [Mattermost][mattermost], [Slack][slack] or [Discord][discord] webhook endpoint. > Note: that because the payload have a `text` attribute, you can use a [Mattermost][mattermost], [Slack][slack] or [Discord][discord] webhook endpoint.
[mattermost]: https://docs.mattermost.com/developer/webhooks-incoming.html [mattermost]: https://docs.mattermost.com/developer/webhooks-incoming.html
[discord]: https://discord.com/developers/docs/resources/webhook#execute-slackcompatible-webhook [discord]: https://discord.com/developers/docs/resources/webhook#execute-slackcompatible-webhook
@ -293,8 +295,7 @@ $ htpasswd -B -c .htpasswd api
``` ```
This command will ask for a password and store it in the htpawsswd file. This command will ask for a password and store it in the htpawsswd file.
Please note that by default, the daemon will try to load the `.htpasswd` file. By default, the daemon will try to load the `.htpasswd` file.
But you can override this behavior by specifying the location of the file: But you can override this behavior by specifying the location of the file:
```bash ```bash

View File

@ -11,8 +11,8 @@ import (
"github.com/ncarlier/webhookd/pkg/strcase" "github.com/ncarlier/webhookd/pkg/strcase"
) )
// QueryParamsToShellVars convert URL query parameters to shell vars. // URLValuesToShellVars convert URL values to shell vars.
func QueryParamsToShellVars(q url.Values) []string { func URLValuesToShellVars(q url.Values) []string {
var params []string var params []string
for k, v := range q { for k, v := range q {
var buf bytes.Buffer var buf bytes.Buffer

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"mime"
"net/http" "net/http"
"path" "path"
"path/filepath" "path/filepath"
@ -51,7 +52,7 @@ func triggerWebhook(w http.ResponseWriter, r *http.Request) {
// Check that streaming is supported // Check that streaming is supported
flusher, ok := w.(http.Flusher) flusher, ok := w.(http.Flusher)
if !ok { if !ok {
http.Error(w, "Streaming not supported!", http.StatusInternalServerError) http.Error(w, "streaming not supported", http.StatusInternalServerError)
return return
} }
@ -68,14 +69,28 @@ func triggerWebhook(w http.ResponseWriter, r *http.Request) {
return return
} }
body, err := ioutil.ReadAll(r.Body) if err = r.ParseForm(); err != nil {
if err != nil { logger.Error.Printf("error reading from-data: %v", err)
logger.Error.Printf("error reading body: %v", err) http.Error(w, "unable to parse request form", http.StatusBadRequest)
http.Error(w, "can't read body", http.StatusBadRequest)
return return
} }
params := QueryParamsToShellVars(r.URL.Query()) // parse body
var body []byte
ct := r.Header.Get("Content-Type")
if ct != "" {
mediatype, _, _ := mime.ParseMediaType(ct)
if strings.HasPrefix(mediatype, "text/") || mediatype == "application/json" {
body, err = ioutil.ReadAll(r.Body)
if err != nil {
logger.Error.Printf("error reading body: %v", err)
http.Error(w, "unable to read request body", http.StatusBadRequest)
return
}
}
}
params := URLValuesToShellVars(r.Form)
params = append(params, HTTPHeadersToShellVars(r.Header)...) params = append(params, HTTPHeadersToShellVars(r.Header)...)
// logger.Debug.Printf("API REQUEST: \"%s\" with params %s...\n", p, params) // logger.Debug.Printf("API REQUEST: \"%s\" with params %s...\n", p, params)

View File

@ -14,7 +14,7 @@ func TestQueryParamsToShellVars(t *testing.T) {
"string": []string{"foo"}, "string": []string{"foo"},
"list": []string{"foo", "bar"}, "list": []string{"foo", "bar"},
} }
values := api.QueryParamsToShellVars(tc) values := api.URLValuesToShellVars(tc)
assert.ContainsStr(t, "string=foo", values, "") assert.ContainsStr(t, "string=foo", values, "")
assert.ContainsStr(t, "list=foo,bar", values, "") assert.ContainsStr(t, "list=foo,bar", values, "")
} }