diff --git a/README.md b/README.md index b56d16e..574c4ca 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,31 @@ go build -mod=vendor . ./git-deploy run --listen :3000 --serve-path ./overrides ``` +## Add Webhooks + +To add a webhook you'll first need a secret + +**with node.js**: + +```js +crypto.randomBytes(16).toString("hex"); +``` + +Then you'll need to set up the webhook in your platform of choice. + +### Github + +New Webhook: `https://github.com/YOUR_ORG/YOUR_REPO/settings/hooks/new` + +```txt +Payload URL: https://YOUR_DOMAIN/api/webhooks/github +Content-Type: application/json +Secret: YOUR_SECRET +Which events would you like to trigger this webhook? +Just the `push` event. +Active: ✅ +``` + ## TODO **git-deploy** is intended for use with static websites that are generated after @@ -44,6 +69,39 @@ don't want to use it. don't want to use it. The built-in interface requires the built-in authentication. +## How to Generate a Base64 Secret + +**in your browser**: + +```js +(async function () { + var rnd = new Uint8Array(16); + await crypto.getRandomValues(rnd); + var b64 = [].slice + .apply(rnd) + .map(function (ch) { + return String.fromCharCode(ch); + }) + .join(""); + var secret = btoa(b64) + .replace(/\//g, "_") + .replace(/\+/g, "-") + .replace(/=/g, ""); + console.info(secret); +})(); +``` + +**with node.js**: + +```js +crypto + .randomBytes(16) + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); +``` + ## License Copyright 2020. All rights reserved. diff --git a/github.go b/github.go new file mode 100644 index 0000000..e602499 --- /dev/null +++ b/github.go @@ -0,0 +1,8 @@ +// // +build github +// TODO omit github unless specified by build tag + +package main + +import ( + _ "git.ryanburnette.com/ryanburnette/git-deploy/internal/webhooks/github" +) diff --git a/internal/options/options.go b/internal/options/options.go new file mode 100644 index 0000000..15afdf4 --- /dev/null +++ b/internal/options/options.go @@ -0,0 +1,23 @@ +package options + +import ( + "flag" +) + +var Server *ServerConfig + +type ServerConfig struct { + Addr string + TrustProxy bool + Compress bool + ServePath string +} + +var ServerFlags *flag.FlagSet +var InitFlags *flag.FlagSet +var DefaultMaxBodySize int64 = 1024 * 1024 + +func init() { + Server = &ServerConfig{} + ServerFlags = flag.NewFlagSet("run", flag.ExitOnError) +} diff --git a/webhooks_github.go b/internal/webhooks/github/github.go similarity index 66% rename from webhooks_github.go rename to internal/webhooks/github/github.go index 86779ed..0e1d07a 100644 --- a/webhooks_github.go +++ b/internal/webhooks/github/github.go @@ -1,6 +1,4 @@ -// // +build github - -package main +package github import ( "fmt" @@ -9,17 +7,23 @@ import ( "net/http" "os" + "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() { - githubSecret := "" - runFlags.StringVar(&githubSecret, "github-secret", "", "secret for github webhooks (same as GITHUB_SECRET=)") - webhookProviders["github"] = registerGithubish("github", &githubSecret, "GITHUB_SECRET") + var githubSecret string + options.ServerFlags.StringVar( + &githubSecret, "github-secret", "", + "secret for github webhooks (same as GITHUB_SECRET=)", + ) + webhooks.AddProvider("github", InitWebhook("github", &githubSecret, "GITHUB_SECRET")) } -func registerGithubish(providername string, secret *string, envname string) func() { +func InitWebhook(providername string, secret *string, envname string) func() { return func() { if "" == *secret { *secret = os.Getenv(envname) @@ -29,9 +33,9 @@ func registerGithubish(providername string, secret *string, envname string) func return } githubSecretB := []byte(*secret) - webhooks[providername] = func(router chi.Router) { + webhooks.AddRouteHandler(providername, func(router chi.Router) { router.Post("/", func(w http.ResponseWriter, r *http.Request) { - body := http.MaxBytesReader(w, r.Body, maxBodySize) + body := http.MaxBytesReader(w, r.Body, options.DefaultMaxBodySize) defer func() { _ = body.Close() }() @@ -63,13 +67,13 @@ func registerGithubish(providername string, secret *string, envname string) func ref := e.GetRef() // *e.Ref branch := ref[len("refs/heads/"):] - hooks <- webhook{ - rev: e.GetAfter(), // *e.After - ref: ref, - branch: branch, - repo: e.GetRepo().GetName(), // *e.Repo.Name - org: e.GetRepo().GetOrganization(), // *e.Repo.Organization - } + webhooks.Hook(webhooks.Ref{ + Rev: e.GetAfter(), // *e.After + Ref: ref, + Branch: branch, + Repo: e.GetRepo().GetName(), // *e.Repo.Name + Org: e.GetRepo().GetOrganization(), // *e.Repo.Organization + }) /* case *github.PullRequestEvent: // probably doesn't matter @@ -84,6 +88,6 @@ func registerGithubish(providername string, secret *string, envname string) func } }) - } + }) } } diff --git a/internal/webhooks/webhooks.go b/internal/webhooks/webhooks.go new file mode 100644 index 0000000..3e0e503 --- /dev/null +++ b/internal/webhooks/webhooks.go @@ -0,0 +1,60 @@ +package webhooks + +import ( + "github.com/go-chi/chi" +) + +// Ref represents typical git webhook info such as: +// HTTPSURL ex: https://git@git.example.com/example/example.git +// SSHURL ex: ssh://git@git.example.com/example/example.git +// Rev ex: 00000000 +// Ref ex: /refs/heads/master +// Branch ex: master +// Repo ex: example +// Org ex: example +type Ref struct { + HTTPSURL string + SSHURL string + Rev string + Ref string + Branch string + Repo string + Org string +} + +var Providers = make(map[string]func()) +var Webhooks = make(map[string]func(chi.Router)) + +var hooks = make(chan Ref) + +func Hook(r Ref) { + hooks <- r +} + +func Accept() Ref { + return <-hooks +} + +func AddProvider(name string, initProvider func()) { + Providers[name] = initProvider +} + +func AddRouteHandler(name string, route func(router chi.Router)) { + Webhooks[name] = route +} + +func MustRegisterAll() { + for _, addHandler := range Providers { + addHandler() + } +} + +func RouteHandlers(r chi.Router) { + r.Route("/api/webhooks", func(r chi.Router) { + for provider, handler := range Webhooks { + r.Route("/"+provider, func(r chi.Router) { + handler(r) + }) + } + }) +} diff --git a/main.go b/main.go index 3c28426..171ab1c 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,8 @@ import ( "time" "git.ryanburnette.com/ryanburnette/git-deploy/assets" + "git.ryanburnette.com/ryanburnette/git-deploy/internal/options" + "git.ryanburnette.com/ryanburnette/git-deploy/internal/webhooks" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" @@ -35,39 +37,18 @@ func ver() { fmt.Printf("%s v%s %s (%s)\n", name, version, commit[:7], date) } -type runOptions struct { - listen string - trustProxy bool - compress bool - static string -} - +var runOpts *options.ServerConfig var runFlags *flag.FlagSet -var runOpts runOptions var initFlags *flag.FlagSet -var webhookProviders = make(map[string]func()) -var webhooks = make(map[string]func(chi.Router)) -var maxBodySize int64 = 1024 * 1024 - -var hooks chan webhook - -type webhook struct { - rev string - ref string - branch string - repo string - org string -} - func init() { - hooks = make(chan webhook) - runOpts = runOptions{} - runFlags = flag.NewFlagSet("run", flag.ExitOnError) - runFlags.StringVar(&runOpts.listen, "listen", ":3000", "the address and port on which to listen") - runFlags.BoolVar(&runOpts.trustProxy, "trust-proxy", false, "trust X-Forwarded-For header") - runFlags.BoolVar(&runOpts.compress, "compress", true, "enable compression for text,html,js,css,etc") - runFlags.StringVar(&runOpts.static, "serve-path", "", "path to serve, falls back to built-in web app") + runOpts = options.Server + runFlags = options.ServerFlags + initFlags = options.InitFlags + runFlags.StringVar(&runOpts.Addr, "listen", ":3000", "the address and port on which to listen") + runFlags.BoolVar(&runOpts.TrustProxy, "trust-proxy", false, "trust X-Forwarded-For header") + runFlags.BoolVar(&runOpts.Compress, "compress", true, "enable compression for text,html,js,css,etc") + runFlags.StringVar(&runOpts.ServePath, "serve-path", "", "path to serve, falls back to built-in web app") } func main() { @@ -99,7 +80,7 @@ func main() { initFlags.Parse(args[2:]) case "run": runFlags.Parse(args[2:]) - registerWebhooks() + webhooks.MustRegisterAll() serve() default: usage() @@ -112,10 +93,10 @@ func serve() { r := chi.NewRouter() // A good base middleware stack - if runOpts.trustProxy { + if runOpts.TrustProxy { r.Use(middleware.RealIP) } - if runOpts.compress { + if runOpts.Compress { r.Use(middleware.Compress(flate.DefaultCompression)) } r.Use(middleware.Logger) @@ -125,9 +106,9 @@ func serve() { var staticHandler http.HandlerFunc pub := http.FileServer(assets.Assets) - if len(runOpts.static) > 0 { + if len(runOpts.ServePath) > 0 { // try the user-provided directory first, then fallback to the built-in - devFS := http.Dir(runOpts.static) + devFS := http.Dir(runOpts.ServePath) dev := http.FileServer(devFS) staticHandler = func(w http.ResponseWriter, r *http.Request) { if _, err := devFS.Open(r.URL.Path); nil != err { @@ -142,13 +123,13 @@ func serve() { } } - loadWebhooks(r) + webhooks.RouteHandlers(r) r.Get("/*", staticHandler) - fmt.Println("Listening for http (with reasonable timeouts) on", runOpts.listen) + fmt.Println("Listening for http (with reasonable timeouts) on", runOpts.Addr) srv := &http.Server{ - Addr: runOpts.listen, + Addr: runOpts.Addr, Handler: r, ReadHeaderTimeout: 2 * time.Second, ReadTimeout: 10 * time.Second, @@ -158,11 +139,11 @@ func serve() { go func() { for { - hook := <-hooks + hook := webhooks.Accept() // TODO os.Exec - fmt.Println(hook.org) - fmt.Println(hook.repo) - fmt.Println(hook.branch) + fmt.Println(hook.Org) + fmt.Println(hook.Repo) + fmt.Println(hook.Branch) } }() @@ -172,19 +153,3 @@ func serve() { return } } - -func registerWebhooks() { - for _, add := range webhookProviders { - add() - } -} - -func loadWebhooks(r chi.Router) { - r.Route("/api/webhooks", func(r chi.Router) { - for provider, handler := range webhooks { - r.Route("/"+provider, func(r chi.Router) { - handler(r) - }) - } - }) -}