mirror of
https://github.com/adnanh/webhook.git
synced 2026-07-26 17:19:17 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 005e723b23 | |||
| 9977fa8c61 | |||
| cbe2440cda | |||
| 9c545a745f | |||
| 263c75b1b5 | |||
| 83cbffd37c | |||
| b310b79fb8 | |||
| f1ebc440a4 |
@@ -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?
|
||||
[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.
|
||||
|
||||
|
||||
+56
-4
@@ -16,10 +16,13 @@ import (
|
||||
|
||||
// Constants used to specify the parameter source
|
||||
const (
|
||||
SourceHeader string = "header"
|
||||
SourceQuery string = "url"
|
||||
SourcePayload string = "payload"
|
||||
SourceString string = "string"
|
||||
SourceHeader string = "header"
|
||||
SourceQuery string = "url"
|
||||
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
|
||||
@@ -151,6 +154,30 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
|
||||
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 {
|
||||
@@ -166,6 +193,7 @@ type Hook struct {
|
||||
ExecuteCommand string `json:"execute-command"`
|
||||
CommandWorkingDirectory string `json:"command-working-directory"`
|
||||
ResponseMessage string `json:"response-message"`
|
||||
CaptureCommandOutput bool `json:"include-command-output-in-response"`
|
||||
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command"`
|
||||
JSONStringParameters []Argument `json:"parse-parameters-as-json"`
|
||||
TriggerRule *Rules `json:"trigger-rule"`
|
||||
@@ -260,6 +288,23 @@ func (h *Hooks) Match(id string) *Hook {
|
||||
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
|
||||
type Rules struct {
|
||||
And *AndRule `json:"and"`
|
||||
@@ -368,3 +413,10 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod
|
||||
}
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"response-message": "I got the payload!",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
|
||||
+50
-26
@@ -25,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
version = "2.3.3"
|
||||
version = "2.3.4"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -144,10 +144,10 @@ func main() {
|
||||
func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
hook := hooks.Match(id)
|
||||
matchedHooks := hooks.MatchAll(id)
|
||||
|
||||
if hook != nil {
|
||||
log.Printf("%s got matched\n", 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 {
|
||||
@@ -165,7 +165,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
|
||||
if strings.HasPrefix(contentType, "application/json") {
|
||||
if strings.Contains(contentType, "json") {
|
||||
decoder := json.NewDecoder(strings.NewReader(string(body)))
|
||||
decoder.UseNumber()
|
||||
|
||||
@@ -174,7 +174,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
log.Printf("error parsing JSON payload %+v\n", err)
|
||||
}
|
||||
} else if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
|
||||
} else if strings.Contains(contentType, "form") {
|
||||
fd, err := url.ParseQuery(string(body))
|
||||
if err != nil {
|
||||
log.Printf("error parsing form payload %+v\n", err)
|
||||
@@ -183,40 +183,64 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
hook.ParseJSONParameters(&headers, &query, &payload)
|
||||
|
||||
// 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
|
||||
fmt.Fprintf(w, hook.ResponseMessage)
|
||||
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(hook *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) {
|
||||
if hook.TriggerRule == nil || hook.TriggerRule != nil && hook.TriggerRule.Evaluate(headers, query, payload, body) {
|
||||
log.Printf("%s hook triggered successfully\n", hook.ID)
|
||||
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
|
||||
|
||||
cmd := exec.Command(hook.ExecuteCommand)
|
||||
cmd.Args = hook.ExtractCommandArguments(headers, query, payload)
|
||||
cmd.Dir = hook.CommandWorkingDirectory
|
||||
log.Printf("executing %s (%s) with arguments %s using %s as cwd\n", h.ExecuteCommand, cmd.Path, cmd.Args, cmd.Dir)
|
||||
|
||||
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 {
|
||||
log.Printf("stderr: %+v\n", err)
|
||||
}
|
||||
log.Printf("finished handling %s\n", hook.ID)
|
||||
} else {
|
||||
log.Printf("%s hook did not get triggered\n", hook.ID)
|
||||
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() {
|
||||
|
||||
+52
-27
@@ -1,4 +1,4 @@
|
||||
//+build windows
|
||||
//+build !windows
|
||||
|
||||
package main
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
version = "2.3.3"
|
||||
version = "2.3.4"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -38,6 +38,7 @@ var (
|
||||
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
|
||||
)
|
||||
@@ -133,10 +134,10 @@ func main() {
|
||||
func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
hook := hooks.Match(id)
|
||||
matchedHooks := hooks.MatchAll(id)
|
||||
|
||||
if hook != nil {
|
||||
log.Printf("%s got matched\n", 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 {
|
||||
@@ -154,7 +155,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
|
||||
if strings.HasPrefix(contentType, "application/json") {
|
||||
if strings.Contains(contentType, "json") {
|
||||
decoder := json.NewDecoder(strings.NewReader(string(body)))
|
||||
decoder.UseNumber()
|
||||
|
||||
@@ -163,7 +164,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
log.Printf("error parsing JSON payload %+v\n", err)
|
||||
}
|
||||
} else if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
|
||||
} else if strings.Contains(contentType, "form") {
|
||||
fd, err := url.ParseQuery(string(body))
|
||||
if err != nil {
|
||||
log.Printf("error parsing form payload %+v\n", err)
|
||||
@@ -172,40 +173,64 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
hook.ParseJSONParameters(&headers, &query, &payload)
|
||||
|
||||
// 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
|
||||
fmt.Fprintf(w, hook.ResponseMessage)
|
||||
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(hook *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) {
|
||||
if hook.TriggerRule == nil || hook.TriggerRule != nil && hook.TriggerRule.Evaluate(headers, query, payload, body) {
|
||||
log.Printf("%s hook triggered successfully\n", hook.ID)
|
||||
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
|
||||
|
||||
cmd := exec.Command(hook.ExecuteCommand)
|
||||
cmd.Args = hook.ExtractCommandArguments(headers, query, payload)
|
||||
cmd.Dir = hook.CommandWorkingDirectory
|
||||
log.Printf("executing %s (%s) with arguments %s using %s as cwd\n", h.ExecuteCommand, cmd.Path, cmd.Args, cmd.Dir)
|
||||
|
||||
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 {
|
||||
log.Printf("stderr: %+v\n", err)
|
||||
}
|
||||
log.Printf("finished handling %s\n", hook.ID)
|
||||
} else {
|
||||
log.Printf("%s hook did not get triggered\n", hook.ID)
|
||||
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() {
|
||||
|
||||
Reference in New Issue
Block a user