Skip to content

Commit

Permalink
Merge pull request #30 from adnanh/development
Browse files Browse the repository at this point in the history
separated windows and other platforms to different files, removed sig…
  • Loading branch information
adnanh committed May 16, 2015
2 parents 6053f48 + 4350685 commit 10732bd
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 2 deletions.
9 changes: 8 additions & 1 deletion hook/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const (
SourceHeader string = "header"
SourceQuery string = "url"
SourcePayload string = "payload"
SourceString string = "string"
)

// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
Expand Down Expand Up @@ -148,6 +149,8 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
source = query
case SourcePayload:
source = payload
case SourceString:
return ha.Name, true
}

if source != nil {
Expand Down Expand Up @@ -194,7 +197,11 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
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 {
log.Printf("couldn't retrieve argument for %+v\n", h.JSONStringParameters[i])
Expand Down
2 changes: 2 additions & 0 deletions hook/hook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ var argumentGetTests = []struct {
{"header", "a", &map[string]interface{}{"a": "z"}, nil, 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},
{"string", "a", nil, nil, &map[string]interface{}{"a": "z"}, "a", true},
// failures
{"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
Expand Down Expand Up @@ -99,6 +100,7 @@ var hookParseJSONParametersTests = []struct {
// 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) {
Expand Down
4 changes: 3 additions & 1 deletion webhook.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//+build !windows

package main

import (
Expand All @@ -23,7 +25,7 @@ import (
)

const (
version = "2.3.2"
version = "2.3.3"
)

var (
Expand Down
258 changes: 258 additions & 0 deletions webhook_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
//+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.3"
)

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

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"]

hook := hooks.Match(id)

if hook != nil {
log.Printf("%s got matched\n", id)

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.HasPrefix(contentType, "application/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.HasPrefix(contentType, "application/x-www-form-urlencoded") {
fd, err := url.ParseQuery(string(body))
if err != nil {
log.Printf("error parsing form payload %+v\n", err)
} else {
payload = valuesToMap(fd)
}
}

hook.ParseJSONParameters(&headers, &query, &payload)

// handle hook
go handleHook(hook, &headers, &query, &payload, &body)

// send the hook defined response message
fmt.Fprintf(w, hook.ResponseMessage)
} 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)

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", hook.ExecuteCommand, cmd.Path, cmd.Args, cmd.Dir)

out, err := cmd.Output()

log.Printf("stdout: %s\n", out)

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)
}
}

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
}

0 comments on commit 10732bd

Please sign in to comment.