From c07056d9fd9a0e6f43d981b52d5dbabc02885ad1 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 29 Sep 2020 02:08:35 -0600 Subject: [PATCH] add bitbucket, update build tags --- README.md | 11 + bitbucket.go | 7 + github.go | 3 +- internal/webhooks/bitbucket/bitbucket.go | 144 +++++++++++++ internal/webhooks/bitbucket/payload.go | 245 +++++++++++++++++++++++ internal/webhooks/github/github.go | 2 + main.go | 6 +- 7 files changed, 413 insertions(+), 5 deletions(-) create mode 100644 bitbucket.go create mode 100644 internal/webhooks/bitbucket/bitbucket.go create mode 100644 internal/webhooks/bitbucket/payload.go diff --git a/README.md b/README.md index 8747438..8180c90 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,17 @@ Just the `push` event. Active: ✅ ``` +### Bitbucket + +Sometimes Bitbucket does not give you the option to specify the (`X-Hub-Signature`) `secret`, +so you'll have to append an `access_token` instead. Example: + +```txt +Title: git-deploy +URL: https://YOUR_DOMAIN/api/webhooks/bitbucket?access_token=YOUR_SECRET +Triggers: Repository push +``` + ## TODO **git-deploy** is intended for use with static websites that are generated after diff --git a/bitbucket.go b/bitbucket.go new file mode 100644 index 0000000..38371e0 --- /dev/null +++ b/bitbucket.go @@ -0,0 +1,7 @@ +// +build !nobitbucket + +package main + +import ( + _ "git.ryanburnette.com/ryanburnette/git-deploy/internal/webhooks/bitbucket" +) diff --git a/github.go b/github.go index e602499..ccfb070 100644 --- a/github.go +++ b/github.go @@ -1,5 +1,4 @@ -// // +build github -// TODO omit github unless specified by build tag +// +build !nogithub package main diff --git a/internal/webhooks/bitbucket/bitbucket.go b/internal/webhooks/bitbucket/bitbucket.go new file mode 100644 index 0000000..d865798 --- /dev/null +++ b/internal/webhooks/bitbucket/bitbucket.go @@ -0,0 +1,144 @@ +package bitbucket + +import ( + "crypto/subtle" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "strings" + + "git.ryanburnette.com/ryanburnette/git-deploy/internal/options" + "git.ryanburnette.com/ryanburnette/git-deploy/internal/webhooks" + + "github.com/go-chi/chi" + "github.com/google/go-github/v32/github" +) + +func init() { + var secret string + name := "bitbucket" + options.ServerFlags.StringVar( + &secret, fmt.Sprintf("%s-secret", name), "", + fmt.Sprintf( + "secret for %s webhooks (same as %s_SECRET=)", + name, strings.ToUpper(name)), + ) + webhooks.AddProvider("bitbucket", InitWebhook("bitbucket", &secret, "BITBUCKET_SECRET")) +} + +// InitWebhook prepares the webhook router. +// It should be called after arguments are parsed and ENVs are set.InitWebhook +func InitWebhook(providername string, secret *string, envname string) func() { + return func() { + if "" == *secret { + *secret = os.Getenv(envname) + } + if "" == *secret { + fmt.Fprintf(os.Stderr, "skipped route for missing %s\n", envname) + return + } + secretB := []byte(*secret) + webhooks.AddRouteHandler(providername, func(router chi.Router) { + router.Post("/", func(w http.ResponseWriter, r *http.Request) { + body := http.MaxBytesReader(w, r.Body, options.DefaultMaxBodySize) + defer func() { + _ = body.Close() + }() + + accessToken := r.URL.Query().Get("access_token") + if "" != accessToken { + if 0 == subtle.ConstantTimeCompare( + []byte(r.URL.Query().Get("access_token")), + secretB, + ) { + log.Printf("invalid bitbucket access_token\n") + http.Error(w, "invalid access_token", http.StatusBadRequest) + return + } + } + + payload, err := ioutil.ReadAll(r.Body) + if err != nil { + // if there's a read error, it should have been handled + // already by the MaxBytesReader + return + } + + if "" == accessToken { + sig := r.Header.Get("X-Hub-Signature") + // TODO replace with generic X-Hub-Signature validation + if err := github.ValidateSignature(sig, payload, secretB); nil != err { + log.Printf("invalid bitbucket signature: error: %s\n", err) + http.Error(w, "invalid bitbucket signature", http.StatusBadRequest) + return + } + } + + info := Webhook{} + if err := json.Unmarshal(payload, &info); nil != err { + log.Printf("invalid bitbucket payload: error: %s\n%s\n", err, string(payload)) + http.Error(w, "invalid bitbucket payload", http.StatusBadRequest) + return + } + + var branch string + var tag string + var ref string + + n := len(info.Push.Changes) + if n < 1 { + log.Printf("invalid bitbucket changeset (n): %d\n%s\n", n, string(payload)) + http.Error(w, "invalid bitbucket payload", http.StatusBadRequest) + return + } else if n > 1 { + log.Printf("more than one bitbucket changeset (n): %d\n%s\n", n, string(payload)) + } + + refName := info.Push.Changes[0].New.Name + refType := info.Push.Changes[0].New.Type + switch refType { + case "tag": + tag = refName + ref = fmt.Sprintf("refs/tags/%s", refName) + case "branch": + branch = refName + ref = fmt.Sprintf("refs/heads/%s", refName) + default: + log.Println("unexpected bitbucket RefType", refType) + ref = fmt.Sprintf("refs/UNKNOWN/%s", refName) + } + + switch refType { + case "tags": + refType = "tag" + tag = refName + case "heads": + refType = "branch" + branch = refName + } + + var rev string + if len(info.Push.Changes[0].Commits) > 0 { + // TODO first or last? + // TODO shouldn't tags have a Commit as well? + rev = info.Push.Changes[0].Commits[0].Hash + } + + webhooks.Hook(webhooks.Ref{ + HTTPSURL: info.Repository.Links.HTML.Href, + Rev: rev, + Ref: ref, + RefType: refType, + RefName: refName, + Branch: branch, + Tag: tag, + Repo: info.Repository.Name, + Owner: info.Repository.Workspace.Slug, + }) + }) + }) + } +} diff --git a/internal/webhooks/bitbucket/payload.go b/internal/webhooks/bitbucket/payload.go new file mode 100644 index 0000000..6e601d7 --- /dev/null +++ b/internal/webhooks/bitbucket/payload.go @@ -0,0 +1,245 @@ +package bitbucket + +import "time" + +// Thank you Matt! +// See https://mholt.github.io/json-to-go/ + +type Webhook struct { + Push Push `json:"push"` + Actor Actor `json:"actor"` + Repository Repository `json:"repository"` +} + +type Push struct { + Changes []struct { + Forced bool `json:"forced"` + Old struct { + Name string `json:"name"` + Type string `json:"type"` + Target struct { + Hash string `json:"hash"` + Author struct { + User struct { + DisplayName string `json:"display_name"` + UUID string `json:"uuid"` + Nickname string `json:"nickname"` + AccountID string `json:"account_id"` + } `json:"user"` + } `json:"author"` + Date time.Time `json:"date"` + Message string `json:"message"` + Type string `json:"type"` + } `json:"target"` + } `json:"old"` + Links struct { + HTML struct { + Href string `json:"href"` + } `json:"html"` + } `json:"links"` + Created bool `json:"created"` + Commits []struct { + Rendered struct { + } `json:"rendered"` + Hash string `json:"hash"` + Links struct { + Self struct { + Href string `json:"href"` + } `json:"self"` + Comments struct { + Href string `json:"href"` + } `json:"comments"` + Patch struct { + Href string `json:"href"` + } `json:"patch"` + HTML struct { + Href string `json:"href"` + } `json:"html"` + Diff struct { + Href string `json:"href"` + } `json:"diff"` + Approve struct { + Href string `json:"href"` + } `json:"approve"` + Statuses struct { + Href string `json:"href"` + } `json:"statuses"` + } `json:"links"` + Author struct { + Raw string `json:"raw"` + Type string `json:"type"` + User struct { + DisplayName string `json:"display_name"` + UUID string `json:"uuid"` + Links struct { + Self struct { + Href string `json:"href"` + } `json:"self"` + HTML struct { + Href string `json:"href"` + } `json:"html"` + Avatar struct { + Href string `json:"href"` + } `json:"avatar"` + } `json:"links"` + Nickname string `json:"nickname"` + Type string `json:"type"` + AccountID string `json:"account_id"` + } `json:"user"` + } `json:"author"` + Summary struct { + Raw string `json:"raw"` + Markup string `json:"markup"` + HTML string `json:"html"` + Type string `json:"type"` + } `json:"summary"` + Parents []struct { + Hash string `json:"hash"` + Type string `json:"type"` + Links struct { + Self struct { + Href string `json:"href"` + } `json:"self"` + HTML struct { + Href string `json:"href"` + } `json:"html"` + } `json:"links"` + } `json:"parents"` + Date time.Time `json:"date"` + Message string `json:"message"` + Type string `json:"type"` + Properties struct { + } `json:"properties"` + } `json:"commits"` + Truncated bool `json:"truncated"` + Closed bool `json:"closed"` + New struct { + Name string `json:"name"` + Links struct { + Commits struct { + Href string `json:"href"` + } `json:"commits"` + Self struct { + Href string `json:"href"` + } `json:"self"` + HTML struct { + Href string `json:"href"` + } `json:"html"` + } `json:"links"` + DefaultMergeStrategy string `json:"default_merge_strategy"` + MergeStrategies []string `json:"merge_strategies"` + Type string `json:"type"` + Target struct { + Rendered struct { + } `json:"rendered"` + Hash string `json:"hash"` + Links struct { + Self struct { + Href string `json:"href"` + } `json:"self"` + HTML struct { + Href string `json:"href"` + } `json:"html"` + } `json:"links"` + Author struct { + Raw string `json:"raw"` + Type string `json:"type"` + User struct { + DisplayName string `json:"display_name"` + UUID string `json:"uuid"` + Links struct { + Self struct { + Href string `json:"href"` + } `json:"self"` + HTML struct { + Href string `json:"href"` + } `json:"html"` + Avatar struct { + Href string `json:"href"` + } `json:"avatar"` + } `json:"links"` + Nickname string `json:"nickname"` + Type string `json:"type"` + AccountID string `json:"account_id"` + } `json:"user"` + } `json:"author"` + Summary struct { + Raw string `json:"raw"` + Markup string `json:"markup"` + HTML string `json:"html"` + Type string `json:"type"` + } `json:"summary"` + Parents []struct { + Hash string `json:"hash"` + Type string `json:"type"` + Links struct { + Self struct { + Href string `json:"href"` + } `json:"self"` + HTML struct { + Href string `json:"href"` + } `json:"html"` + } `json:"links"` + } `json:"parents"` + Date time.Time `json:"date"` + Message string `json:"message"` + Type string `json:"type"` + Properties struct { + } `json:"properties"` + } `json:"target"` + } `json:"new"` + } `json:"changes"` +} + +type Actor struct { + DisplayName string `json:"display_name"` + UUID string `json:"uuid"` + Nickname string `json:"nickname"` + Type string `json:"type"` + AccountID string `json:"account_id"` +} + +type Repository struct { + Name string `json:"name"` + Scm string `json:"scm"` + Website interface{} `json:"website"` + UUID string `json:"uuid"` + Links struct { + Self struct { + Href string `json:"href"` + } `json:"self"` + HTML struct { + Href string `json:"href"` + } `json:"html"` + Avatar struct { + Href string `json:"href"` + } `json:"avatar"` + } `json:"links"` + FullName string `json:"full_name"` + Owner struct { + DisplayName string `json:"display_name"` + UUID string `json:"uuid"` + Links struct { + Self struct { + Href string `json:"href"` + } `json:"self"` + HTML struct { + Href string `json:"href"` + } `json:"html"` + Avatar struct { + Href string `json:"href"` + } `json:"avatar"` + } `json:"links"` + Nickname string `json:"nickname"` + Type string `json:"type"` + AccountID string `json:"account_id"` + } `json:"owner"` + Workspace struct { + Slug string `json:"slug"` + Type string `json:"type"` + Name string `json:"name"` + UUID string `json:"uuid"` + } `json:"workspace"` + Type string `json:"type"` + IsPrivate bool `json:"is_private"` +} diff --git a/internal/webhooks/github/github.go b/internal/webhooks/github/github.go index f8b97ce..ddac5ca 100644 --- a/internal/webhooks/github/github.go +++ b/internal/webhooks/github/github.go @@ -12,6 +12,8 @@ import ( "git.ryanburnette.com/ryanburnette/git-deploy/internal/webhooks" "github.com/go-chi/chi" + // TODO nix this dependency in favor of a lightweight X-Hub-Signature + // and JSON-to-Go-struct approach "github.com/google/go-github/v32/github" ) diff --git a/main.go b/main.go index 7b234a7..40b54de 100644 --- a/main.go +++ b/main.go @@ -93,9 +93,9 @@ func main() { os.Exit(0) return case "init": - initFlags.Parse(args[2:]) + _ = initFlags.Parse(args[2:]) case "run": - runFlags.Parse(args[2:]) + _ = runFlags.Parse(args[2:]) if "" == runOpts.Exec { fmt.Printf("--exec is a required flag") os.Exit(1) @@ -210,7 +210,7 @@ func serve() { go func() { log.Printf("git-deploy job for %s#%s started\n", hook.HTTPSURL, hook.RefName) - cmd.Wait() + _ = cmd.Wait() delete(jobs, jobID) log.Printf("git-deploy job for %s#%s finished\n", hook.HTTPSURL, hook.RefName) }()