mirror of
https://github.com/adnanh/webhook.git
synced 2026-07-27 17:49:50 +08:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 005e723b23 | |||
| 9977fa8c61 | |||
| cbe2440cda | |||
| 9c545a745f | |||
| 263c75b1b5 | |||
| 83cbffd37c | |||
| b310b79fb8 | |||
| f1ebc440a4 | |||
| 10732bd57b | |||
| 4350685330 | |||
| 6053f48b23 | |||
| 6cd8258651 | |||
| fb71ea0fae | |||
| aeacb6dac7 | |||
| 1039151a16 | |||
| db928228c8 | |||
| 6896a34aab | |||
| 5f853d8aba | |||
| 12c48f87cb | |||
| acf38c3210 | |||
| d3f368cb8f | |||
| 943bc258f7 |
@@ -1,3 +1,5 @@
|
|||||||
|
[](https://gitter.im/adnanh/webhook?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
|
|
||||||
# What is webhook?
|
# What is webhook?
|
||||||
[webhook](https://github.com/adnanh/webhook/) is a lightweight configurable tool written in Go, that allows you to easily create HTTP endpoints (hooks) on your server, which you can use to execute configured commands. You can also pass data from the HTTP request (such as headers, payload or query variables) to your commands. [webhook](https://github.com/adnanh/webhook/) also allows you to specify rules which have to be satisfied in order for the hook to be triggered.
|
[webhook](https://github.com/adnanh/webhook/) is a lightweight configurable tool written in Go, that allows you to easily create HTTP endpoints (hooks) on your server, which you can use to execute configured commands. You can also pass data from the HTTP request (such as headers, payload or query variables) to your commands. [webhook](https://github.com/adnanh/webhook/) also allows you to specify rules which have to be satisfied in order for the hook to be triggered.
|
||||||
|
|
||||||
@@ -62,6 +64,8 @@ Check out [Hook examples page](https://github.com/adnanh/webhook/wiki/Hook-Examp
|
|||||||
# Contributing
|
# Contributing
|
||||||
Any form of contribution is welcome and highly appreciated.
|
Any form of contribution is welcome and highly appreciated.
|
||||||
|
|
||||||
|
Big thanks to [all the current contributors](https://github.com/adnanh/webhook/graphs/contributors) for their contributions!
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|||||||
+63
-4
@@ -16,9 +16,13 @@ import (
|
|||||||
|
|
||||||
// Constants used to specify the parameter source
|
// Constants used to specify the parameter source
|
||||||
const (
|
const (
|
||||||
SourceHeader string = "header"
|
SourceHeader string = "header"
|
||||||
SourceQuery string = "url"
|
SourceQuery string = "url"
|
||||||
SourcePayload string = "payload"
|
SourcePayload string = "payload"
|
||||||
|
SourceString string = "string"
|
||||||
|
SourceEntirePayload string = "entire-payload"
|
||||||
|
SourceEntireQuery string = "entire-query"
|
||||||
|
SourceEntireHeaders string = "entire-headers"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
|
// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
|
||||||
@@ -148,6 +152,32 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
|
|||||||
source = query
|
source = query
|
||||||
case SourcePayload:
|
case SourcePayload:
|
||||||
source = payload
|
source = payload
|
||||||
|
case SourceString:
|
||||||
|
return ha.Name, true
|
||||||
|
case SourceEntirePayload:
|
||||||
|
r, err := json.Marshal(payload)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(r), true
|
||||||
|
case SourceEntireHeaders:
|
||||||
|
r, err := json.Marshal(headers)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(r), true
|
||||||
|
case SourceEntireQuery:
|
||||||
|
r, err := json.Marshal(query)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(r), true
|
||||||
}
|
}
|
||||||
|
|
||||||
if source != nil {
|
if source != nil {
|
||||||
@@ -163,6 +193,7 @@ type Hook struct {
|
|||||||
ExecuteCommand string `json:"execute-command"`
|
ExecuteCommand string `json:"execute-command"`
|
||||||
CommandWorkingDirectory string `json:"command-working-directory"`
|
CommandWorkingDirectory string `json:"command-working-directory"`
|
||||||
ResponseMessage string `json:"response-message"`
|
ResponseMessage string `json:"response-message"`
|
||||||
|
CaptureCommandOutput bool `json:"include-command-output-in-response"`
|
||||||
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command"`
|
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command"`
|
||||||
JSONStringParameters []Argument `json:"parse-parameters-as-json"`
|
JSONStringParameters []Argument `json:"parse-parameters-as-json"`
|
||||||
TriggerRule *Rules `json:"trigger-rule"`
|
TriggerRule *Rules `json:"trigger-rule"`
|
||||||
@@ -194,7 +225,11 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
|
|||||||
source = query
|
source = query
|
||||||
}
|
}
|
||||||
|
|
||||||
ReplaceParameter(h.JSONStringParameters[i].Name, source, newArg)
|
if source != nil {
|
||||||
|
ReplaceParameter(h.JSONStringParameters[i].Name, source, newArg)
|
||||||
|
} else {
|
||||||
|
log.Printf("invalid source for argument %+v\n", h.JSONStringParameters[i])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("couldn't retrieve argument for %+v\n", h.JSONStringParameters[i])
|
log.Printf("couldn't retrieve argument for %+v\n", h.JSONStringParameters[i])
|
||||||
@@ -253,6 +288,23 @@ func (h *Hooks) Match(id string) *Hook {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MatchAll iterates through Hooks and returns all of the hooks that match the
|
||||||
|
// given ID, if no hook matches the given ID, nil is returned
|
||||||
|
func (h *Hooks) MatchAll(id string) []*Hook {
|
||||||
|
matchedHooks := make([]*Hook, 0)
|
||||||
|
for i := range *h {
|
||||||
|
if (*h)[i].ID == id {
|
||||||
|
matchedHooks = append(matchedHooks, &(*h)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(matchedHooks) > 0 {
|
||||||
|
return matchedHooks
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Rules is a structure that contains one of the valid rule types
|
// Rules is a structure that contains one of the valid rule types
|
||||||
type Rules struct {
|
type Rules struct {
|
||||||
And *AndRule `json:"and"`
|
And *AndRule `json:"and"`
|
||||||
@@ -361,3 +413,10 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CommandStatusResponse type encapsulates the executed command exit code, message, stdout and stderr
|
||||||
|
type CommandStatusResponse struct {
|
||||||
|
ResponseMessage string `json:"message"`
|
||||||
|
Output string `json:"output"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ var extractParameterTests = []struct {
|
|||||||
{"a.500.b", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "", false}, // non-existent slice
|
{"a.500.b", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "", false}, // non-existent slice
|
||||||
{"a.501.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // non-existent slice index
|
{"a.501.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // non-existent slice index
|
||||||
{"a.502.b", map[string]interface{}{"a": []interface{}{}}, "", false}, // non-existent slice index
|
{"a.502.b", map[string]interface{}{"a": []interface{}{}}, "", false}, // non-existent slice index
|
||||||
|
{"a.b.503", map[string]interface{}{"a": map[string]interface{}{"b": []interface{}{"x", "y", "z"}}}, "", false}, // trailing, non-existent slice index
|
||||||
|
{"a.b", interface{}("a"), "", false}, // non-map, non-slice input
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExtractParameter(t *testing.T) {
|
func TestExtractParameter(t *testing.T) {
|
||||||
@@ -68,6 +70,7 @@ var argumentGetTests = []struct {
|
|||||||
{"header", "a", &map[string]interface{}{"a": "z"}, nil, nil, "z", true},
|
{"header", "a", &map[string]interface{}{"a": "z"}, nil, nil, "z", true},
|
||||||
{"url", "a", nil, &map[string]interface{}{"a": "z"}, nil, "z", true},
|
{"url", "a", nil, &map[string]interface{}{"a": "z"}, nil, "z", true},
|
||||||
{"payload", "a", nil, nil, &map[string]interface{}{"a": "z"}, "z", true},
|
{"payload", "a", nil, nil, &map[string]interface{}{"a": "z"}, "z", true},
|
||||||
|
{"string", "a", nil, nil, &map[string]interface{}{"a": "z"}, "a", true},
|
||||||
// failures
|
// failures
|
||||||
{"header", "a", nil, &map[string]interface{}{"a": "z"}, &map[string]interface{}{"a": "z"}, "", false}, // nil headers
|
{"header", "a", nil, &map[string]interface{}{"a": "z"}, &map[string]interface{}{"a": "z"}, "", false}, // nil headers
|
||||||
{"url", "a", &map[string]interface{}{"a": "z"}, nil, &map[string]interface{}{"a": "z"}, "", false}, // nil query
|
{"url", "a", &map[string]interface{}{"a": "z"}, nil, &map[string]interface{}{"a": "z"}, "", false}, // nil query
|
||||||
@@ -85,6 +88,31 @@ func TestArgumentGet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hookParseJSONParametersTests = []struct {
|
||||||
|
params []Argument
|
||||||
|
headers, query, payload *map[string]interface{}
|
||||||
|
rheaders, rquery, rpayload *map[string]interface{}
|
||||||
|
}{
|
||||||
|
{[]Argument{Argument{"header", "a"}}, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, nil},
|
||||||
|
{[]Argument{Argument{"url", "a"}}, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil},
|
||||||
|
{[]Argument{Argument{"payload", "a"}}, nil, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}},
|
||||||
|
{[]Argument{Argument{"header", "z"}}, &map[string]interface{}{"z": `{}`}, nil, nil, &map[string]interface{}{"z": map[string]interface{}{}}, nil, nil},
|
||||||
|
// failures
|
||||||
|
{[]Argument{Argument{"header", "z"}}, &map[string]interface{}{"z": ``}, nil, nil, &map[string]interface{}{"z": ``}, nil, nil}, // empty string
|
||||||
|
{[]Argument{Argument{"header", "y"}}, &map[string]interface{}{"X": `{}`}, nil, nil, &map[string]interface{}{"X": `{}`}, nil, nil}, // missing parameter
|
||||||
|
{[]Argument{Argument{"string", "z"}}, &map[string]interface{}{"z": ``}, nil, nil, &map[string]interface{}{"z": ``}, nil, nil}, // invalid argument source
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookParseJSONParameters(t *testing.T) {
|
||||||
|
for _, tt := range hookParseJSONParametersTests {
|
||||||
|
h := &Hook{JSONStringParameters: tt.params}
|
||||||
|
h.ParseJSONParameters(tt.headers, tt.query, tt.payload)
|
||||||
|
if !reflect.DeepEqual(tt.headers, tt.rheaders) {
|
||||||
|
t.Errorf("failed to parse %v:\nexpected %#v,\ngot %#v", tt.params, *tt.rheaders, *tt.headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var hookExtractCommandArgumentsTests = []struct {
|
var hookExtractCommandArgumentsTests = []struct {
|
||||||
exec string
|
exec string
|
||||||
args []Argument
|
args []Argument
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"id": "webhook",
|
"id": "webhook",
|
||||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||||
"command-working-directory": "/home/adnan/go",
|
"command-working-directory": "/home/adnan/go",
|
||||||
|
"response-message": "I got the payload!",
|
||||||
"pass-arguments-to-command":
|
"pass-arguments-to-command":
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|||||||
+99
-43
@@ -1,3 +1,5 @@
|
|||||||
|
//+build !windows
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -10,7 +12,9 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/adnanh/webhook/hook"
|
"github.com/adnanh/webhook/hook"
|
||||||
|
|
||||||
@@ -21,7 +25,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
version = "2.3.0"
|
version = "2.3.4"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -36,6 +40,7 @@ var (
|
|||||||
key = flag.String("key", "key.pem", "path to the HTTPS certificate private key pem file")
|
key = flag.String("key", "key.pem", "path to the HTTPS certificate private key pem file")
|
||||||
|
|
||||||
watcher *fsnotify.Watcher
|
watcher *fsnotify.Watcher
|
||||||
|
signals chan os.Signal
|
||||||
|
|
||||||
hooks hook.Hooks
|
hooks hook.Hooks
|
||||||
)
|
)
|
||||||
@@ -54,6 +59,14 @@ func init() {
|
|||||||
|
|
||||||
log.Println("version " + version + " starting")
|
log.Println("version " + version + " starting")
|
||||||
|
|
||||||
|
// set os signal watcher
|
||||||
|
log.Printf("setting up os signal watcher\n")
|
||||||
|
|
||||||
|
signals = make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signals, syscall.SIGUSR1)
|
||||||
|
|
||||||
|
go watchForSignals()
|
||||||
|
|
||||||
// load and parse hooks
|
// load and parse hooks
|
||||||
log.Printf("attempting to load hooks from %s\n", *hooksFilePath)
|
log.Printf("attempting to load hooks from %s\n", *hooksFilePath)
|
||||||
|
|
||||||
@@ -131,10 +144,10 @@ func main() {
|
|||||||
func hookHandler(w http.ResponseWriter, r *http.Request) {
|
func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
id := mux.Vars(r)["id"]
|
id := mux.Vars(r)["id"]
|
||||||
|
|
||||||
hook := hooks.Match(id)
|
matchedHooks := hooks.MatchAll(id)
|
||||||
|
|
||||||
if hook != nil {
|
if matchedHooks != nil {
|
||||||
log.Printf("%s got matched\n", id)
|
log.Printf("%s got matched (%d time(s))\n", id, len(matchedHooks))
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -152,7 +165,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
contentType := r.Header.Get("Content-Type")
|
contentType := r.Header.Get("Content-Type")
|
||||||
|
|
||||||
if contentType == "application/json" {
|
if strings.Contains(contentType, "json") {
|
||||||
decoder := json.NewDecoder(strings.NewReader(string(body)))
|
decoder := json.NewDecoder(strings.NewReader(string(body)))
|
||||||
decoder.UseNumber()
|
decoder.UseNumber()
|
||||||
|
|
||||||
@@ -161,7 +174,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error parsing JSON payload %+v\n", err)
|
log.Printf("error parsing JSON payload %+v\n", err)
|
||||||
}
|
}
|
||||||
} else if contentType == "application/x-www-form-urlencoded" {
|
} else if strings.Contains(contentType, "form") {
|
||||||
fd, err := url.ParseQuery(string(body))
|
fd, err := url.ParseQuery(string(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error parsing form payload %+v\n", err)
|
log.Printf("error parsing form payload %+v\n", err)
|
||||||
@@ -170,39 +183,84 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hook.ParseJSONParameters(&headers, &query, &payload)
|
|
||||||
|
|
||||||
// handle hook
|
// handle hook
|
||||||
go handleHook(hook, &headers, &query, &payload, &body)
|
for _, h := range matchedHooks {
|
||||||
|
h.ParseJSONParameters(&headers, &query, &payload)
|
||||||
|
if h.TriggerRule == nil || h.TriggerRule != nil && h.TriggerRule.Evaluate(&headers, &query, &payload, &body) {
|
||||||
|
log.Printf("%s hook triggered successfully\n", h.ID)
|
||||||
|
|
||||||
// send the hook defined response message
|
if h.CaptureCommandOutput {
|
||||||
fmt.Fprintf(w, hook.ResponseMessage)
|
response := handleHook(h, &headers, &query, &payload, &body)
|
||||||
|
fmt.Fprintf(w, response)
|
||||||
|
} else {
|
||||||
|
go handleHook(h, &headers, &query, &payload, &body)
|
||||||
|
fmt.Fprintf(w, h.ResponseMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if none of the hooks got triggered
|
||||||
|
log.Printf("%s got matched (%d time(s)), but didn't get triggered because the trigger rules were not satisfied\n", matchedHooks[0].ID, len(matchedHooks))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprintf(w, "Hook rules were not satisfied.")
|
||||||
} else {
|
} else {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
fmt.Fprintf(w, "Hook not found.")
|
fmt.Fprintf(w, "Hook not found.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleHook(hook *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) {
|
func handleHook(h *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) string {
|
||||||
if hook.TriggerRule == nil || hook.TriggerRule != nil && hook.TriggerRule.Evaluate(headers, query, payload, body) {
|
cmd := exec.Command(h.ExecuteCommand)
|
||||||
log.Printf("%s hook triggered successfully\n", hook.ID)
|
cmd.Args = h.ExtractCommandArguments(headers, query, payload)
|
||||||
|
cmd.Dir = h.CommandWorkingDirectory
|
||||||
|
|
||||||
cmd := exec.Command(hook.ExecuteCommand)
|
log.Printf("executing %s (%s) with arguments %s using %s as cwd\n", h.ExecuteCommand, cmd.Path, cmd.Args, cmd.Dir)
|
||||||
cmd.Args = hook.ExtractCommandArguments(headers, query, payload)
|
|
||||||
cmd.Dir = hook.CommandWorkingDirectory
|
|
||||||
|
|
||||||
log.Printf("executing %s (%s) with arguments %s using %s as cwd\n", hook.ExecuteCommand, cmd.Path, cmd.Args, cmd.Dir)
|
out, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
out, err := cmd.Output()
|
log.Printf("command output: %s\n", out)
|
||||||
|
|
||||||
log.Printf("stdout: %s\n", out)
|
var errorResponse string
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("stderr: %+v\n", err)
|
log.Printf("error occurred: %+v\n", err)
|
||||||
}
|
errorResponse = fmt.Sprintf("%+v", err)
|
||||||
log.Printf("finished handling %s\n", hook.ID)
|
}
|
||||||
|
|
||||||
|
log.Printf("finished handling %s\n", h.ID)
|
||||||
|
|
||||||
|
var response []byte
|
||||||
|
response, err = json.Marshal(&hook.CommandStatusResponse{ResponseMessage: h.ResponseMessage, Output: string(out), Error: errorResponse})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error marshalling response: %+v", err)
|
||||||
|
return h.ResponseMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadHooks() {
|
||||||
|
newHooks := hook.Hooks{}
|
||||||
|
|
||||||
|
// parse and swap
|
||||||
|
log.Printf("attempting to reload hooks from %s\n", *hooksFilePath)
|
||||||
|
|
||||||
|
err := newHooks.LoadFromFile(*hooksFilePath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("couldn't load hooks from file! %+v\n", err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("%s hook did not get triggered\n", hook.ID)
|
log.Printf("loaded %d hook(s) from file\n", len(hooks))
|
||||||
|
|
||||||
|
for _, hook := range hooks {
|
||||||
|
log.Printf("\t> %s\n", hook.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
hooks = newHooks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,24 +271,7 @@ func watchForFileChange() {
|
|||||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||||
log.Println("hooks file modified")
|
log.Println("hooks file modified")
|
||||||
|
|
||||||
newHooks := hook.Hooks{}
|
reloadHooks()
|
||||||
|
|
||||||
// parse and swap
|
|
||||||
log.Printf("attempting to reload hooks from %s\n", *hooksFilePath)
|
|
||||||
|
|
||||||
err := newHooks.LoadFromFile(*hooksFilePath)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("couldn't load hooks from file! %+v\n", err)
|
|
||||||
} else {
|
|
||||||
log.Printf("loaded %d hook(s) from file\n", len(hooks))
|
|
||||||
|
|
||||||
for _, hook := range hooks {
|
|
||||||
log.Printf("\t> %s\n", hook.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
hooks = newHooks
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case err := <-(*watcher).Errors:
|
case err := <-(*watcher).Errors:
|
||||||
log.Println("watcher error:", err)
|
log.Println("watcher error:", err)
|
||||||
@@ -238,6 +279,21 @@ func watchForFileChange() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func watchForSignals() {
|
||||||
|
log.Println("os signal watcher ready")
|
||||||
|
|
||||||
|
for {
|
||||||
|
sig := <-signals
|
||||||
|
if sig == syscall.SIGUSR1 {
|
||||||
|
log.Println("caught USR1 signal")
|
||||||
|
|
||||||
|
reloadHooks()
|
||||||
|
} else {
|
||||||
|
log.Printf("caught unhandled signal %+v\n", sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// valuesToMap converts map[string][]string to a map[string]string object
|
// valuesToMap converts map[string][]string to a map[string]string object
|
||||||
func valuesToMap(values map[string][]string) map[string]interface{} {
|
func valuesToMap(values map[string][]string) map[string]interface{} {
|
||||||
ret := make(map[string]interface{})
|
ret := make(map[string]interface{})
|
||||||
|
|||||||
@@ -0,0 +1,283 @@
|
|||||||
|
//+build !windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/adnanh/webhook/hook"
|
||||||
|
|
||||||
|
"github.com/codegangsta/negroni"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
fsnotify "gopkg.in/fsnotify.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
version = "2.3.4"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ip = flag.String("ip", "", "ip the webhook should serve hooks on")
|
||||||
|
port = flag.Int("port", 9000, "port the webhook should serve hooks on")
|
||||||
|
verbose = flag.Bool("verbose", false, "show verbose output")
|
||||||
|
hotReload = flag.Bool("hotreload", false, "watch hooks file for changes and reload them automatically")
|
||||||
|
hooksFilePath = flag.String("hooks", "hooks.json", "path to the json file containing defined hooks the webhook should serve")
|
||||||
|
hooksURLPrefix = flag.String("urlprefix", "hooks", "url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id)")
|
||||||
|
secure = flag.Bool("secure", false, "use HTTPS instead of HTTP")
|
||||||
|
cert = flag.String("cert", "cert.pem", "path to the HTTPS certificate pem file")
|
||||||
|
key = flag.String("key", "key.pem", "path to the HTTPS certificate private key pem file")
|
||||||
|
|
||||||
|
watcher *fsnotify.Watcher
|
||||||
|
signals chan os.Signal
|
||||||
|
|
||||||
|
hooks hook.Hooks
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
hooks = hook.Hooks{}
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
log.SetPrefix("[webhook] ")
|
||||||
|
log.SetFlags(log.Ldate | log.Ltime)
|
||||||
|
|
||||||
|
if !*verbose {
|
||||||
|
log.SetOutput(ioutil.Discard)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("version " + version + " starting")
|
||||||
|
|
||||||
|
// load and parse hooks
|
||||||
|
log.Printf("attempting to load hooks from %s\n", *hooksFilePath)
|
||||||
|
|
||||||
|
err := hooks.LoadFromFile(*hooksFilePath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("couldn't load hooks from file! %+v\n", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("loaded %d hook(s) from file\n", len(hooks))
|
||||||
|
|
||||||
|
for _, hook := range hooks {
|
||||||
|
log.Printf("\t> %s\n", hook.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if *hotReload {
|
||||||
|
// set up file watcher
|
||||||
|
log.Printf("setting up file watcher for %s\n", *hooksFilePath)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
watcher, err = fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("error creating file watcher instance", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer watcher.Close()
|
||||||
|
|
||||||
|
go watchForFileChange()
|
||||||
|
|
||||||
|
err = watcher.Add(*hooksFilePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("error adding hooks file to the watcher", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l := negroni.NewLogger()
|
||||||
|
l.Logger = log.New(os.Stdout, "[webhook] ", log.Ldate|log.Ltime)
|
||||||
|
|
||||||
|
negroniRecovery := &negroni.Recovery{
|
||||||
|
Logger: l.Logger,
|
||||||
|
PrintStack: true,
|
||||||
|
StackAll: false,
|
||||||
|
StackSize: 1024 * 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
n := negroni.New(negroniRecovery, l)
|
||||||
|
|
||||||
|
router := mux.NewRouter()
|
||||||
|
|
||||||
|
var hooksURL string
|
||||||
|
|
||||||
|
if *hooksURLPrefix == "" {
|
||||||
|
hooksURL = "/{id}"
|
||||||
|
} else {
|
||||||
|
hooksURL = "/" + *hooksURLPrefix + "/{id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
router.HandleFunc(hooksURL, hookHandler)
|
||||||
|
|
||||||
|
n.UseHandler(router)
|
||||||
|
|
||||||
|
if *secure {
|
||||||
|
log.Printf("starting secure (https) webhook on %s:%d", *ip, *port)
|
||||||
|
log.Fatal(http.ListenAndServeTLS(fmt.Sprintf("%s:%d", *ip, *port), *cert, *key, n))
|
||||||
|
} else {
|
||||||
|
log.Printf("starting insecure (http) webhook on %s:%d", *ip, *port)
|
||||||
|
log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", *ip, *port), n))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := mux.Vars(r)["id"]
|
||||||
|
|
||||||
|
matchedHooks := hooks.MatchAll(id)
|
||||||
|
|
||||||
|
if matchedHooks != nil {
|
||||||
|
log.Printf("%s got matched (%d time(s))\n", id, len(matchedHooks))
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error reading the request body. %+v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse headers
|
||||||
|
headers := valuesToMap(r.Header)
|
||||||
|
|
||||||
|
// parse query variables
|
||||||
|
query := valuesToMap(r.URL.Query())
|
||||||
|
|
||||||
|
// parse body
|
||||||
|
var payload map[string]interface{}
|
||||||
|
|
||||||
|
contentType := r.Header.Get("Content-Type")
|
||||||
|
|
||||||
|
if strings.Contains(contentType, "json") {
|
||||||
|
decoder := json.NewDecoder(strings.NewReader(string(body)))
|
||||||
|
decoder.UseNumber()
|
||||||
|
|
||||||
|
err := decoder.Decode(&payload)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error parsing JSON payload %+v\n", err)
|
||||||
|
}
|
||||||
|
} else if strings.Contains(contentType, "form") {
|
||||||
|
fd, err := url.ParseQuery(string(body))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error parsing form payload %+v\n", err)
|
||||||
|
} else {
|
||||||
|
payload = valuesToMap(fd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle hook
|
||||||
|
for _, h := range matchedHooks {
|
||||||
|
h.ParseJSONParameters(&headers, &query, &payload)
|
||||||
|
if h.TriggerRule == nil || h.TriggerRule != nil && h.TriggerRule.Evaluate(&headers, &query, &payload, &body) {
|
||||||
|
log.Printf("%s hook triggered successfully\n", h.ID)
|
||||||
|
|
||||||
|
if h.CaptureCommandOutput {
|
||||||
|
response := handleHook(h, &headers, &query, &payload, &body)
|
||||||
|
fmt.Fprintf(w, response)
|
||||||
|
} else {
|
||||||
|
go handleHook(h, &headers, &query, &payload, &body)
|
||||||
|
fmt.Fprintf(w, h.ResponseMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if none of the hooks got triggered
|
||||||
|
log.Printf("%s got matched (%d time(s)), but didn't get triggered because the trigger rules were not satisfied\n", matchedHooks[0].ID, len(matchedHooks))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprintf(w, "Hook rules were not satisfied.")
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
fmt.Fprintf(w, "Hook not found.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleHook(h *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) string {
|
||||||
|
cmd := exec.Command(h.ExecuteCommand)
|
||||||
|
cmd.Args = h.ExtractCommandArguments(headers, query, payload)
|
||||||
|
cmd.Dir = h.CommandWorkingDirectory
|
||||||
|
|
||||||
|
log.Printf("executing %s (%s) with arguments %s using %s as cwd\n", h.ExecuteCommand, cmd.Path, cmd.Args, cmd.Dir)
|
||||||
|
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
|
log.Printf("command output: %s\n", out)
|
||||||
|
|
||||||
|
var errorResponse string
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error occurred: %+v\n", err)
|
||||||
|
errorResponse = fmt.Sprintf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("finished handling %s\n", h.ID)
|
||||||
|
|
||||||
|
var response []byte
|
||||||
|
response, err = json.Marshal(&hook.CommandStatusResponse{ResponseMessage: h.ResponseMessage, Output: string(out), Error: errorResponse})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error marshalling response: %+v", err)
|
||||||
|
return h.ResponseMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadHooks() {
|
||||||
|
newHooks := hook.Hooks{}
|
||||||
|
|
||||||
|
// parse and swap
|
||||||
|
log.Printf("attempting to reload hooks from %s\n", *hooksFilePath)
|
||||||
|
|
||||||
|
err := newHooks.LoadFromFile(*hooksFilePath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("couldn't load hooks from file! %+v\n", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("loaded %d hook(s) from file\n", len(hooks))
|
||||||
|
|
||||||
|
for _, hook := range hooks {
|
||||||
|
log.Printf("\t> %s\n", hook.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
hooks = newHooks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func watchForFileChange() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-(*watcher).Events:
|
||||||
|
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||||
|
log.Println("hooks file modified")
|
||||||
|
|
||||||
|
reloadHooks()
|
||||||
|
}
|
||||||
|
case err := <-(*watcher).Errors:
|
||||||
|
log.Println("watcher error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// valuesToMap converts map[string][]string to a map[string]string object
|
||||||
|
func valuesToMap(values map[string][]string) map[string]interface{} {
|
||||||
|
ret := make(map[string]interface{})
|
||||||
|
|
||||||
|
for key, value := range values {
|
||||||
|
if len(value) > 0 {
|
||||||
|
ret[key] = value[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user