diff --git a/cmd/lure-api-server/config.go b/cmd/lure-api-server/config.go new file mode 100644 index 0000000..2416ab3 --- /dev/null +++ b/cmd/lure-api-server/config.go @@ -0,0 +1,16 @@ +package main + +import ( + "go.arsenm.dev/logger/log" + "go.arsenm.dev/lure/internal/config" + "go.arsenm.dev/lure/internal/types" +) + +var cfg types.Config + +func init() { + err := config.Decode(&cfg) + if err != nil { + log.Fatal("Error decoding config file").Err(err).Send() + } +} diff --git a/cmd/lure-api-server/main.go b/cmd/lure-api-server/main.go index 9c3945b..e35e4b4 100644 --- a/cmd/lure-api-server/main.go +++ b/cmd/lure-api-server/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "flag" "net" "net/http" @@ -10,6 +11,7 @@ import ( "go.arsenm.dev/logger" "go.arsenm.dev/logger/log" "go.arsenm.dev/lure/internal/api" + "go.arsenm.dev/lure/internal/repos" ) func init() { @@ -17,6 +19,8 @@ func init() { } func main() { + ctx := context.Background() + addr := flag.String("a", ":8080", "Listen address for API server") logFile := flag.String("l", "", "Output file for JSON log") flag.Parse() @@ -31,10 +35,22 @@ func main() { log.Logger = logger.NewMulti(log.Logger, logger.NewJSON(fl)) } - srv := api.NewAPIServer( + err := repos.Pull(ctx, gdb, cfg.Repos) + if err != nil { + log.Fatal("Error pulling repositories").Err(err).Send() + } + + sigCh := make(chan struct{}, 200) + go repoPullWorker(ctx, sigCh) + + var handler http.Handler + + handler = api.NewAPIServer( lureWebAPI{db: gdb}, twirp.WithServerPathPrefix(""), ) + handler = allowAllCORSHandler(handler) + handler = handleWebhook(handler, sigCh) ln, err := net.Listen("tcp", *addr) if err != nil { @@ -42,8 +58,8 @@ func main() { } log.Info("Starting HTTP API server").Str("addr", ln.Addr().String()).Send() - - err = http.Serve(ln, allowAllCORSHandler(srv)) + + err = http.Serve(ln, handler) if err != nil { log.Fatal("Error while running server").Err(err).Send() } diff --git a/cmd/lure-api-server/webhook.go b/cmd/lure-api-server/webhook.go new file mode 100644 index 0000000..9b6ed0a --- /dev/null +++ b/cmd/lure-api-server/webhook.go @@ -0,0 +1,89 @@ +package main + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "errors" + "io" + "net/http" + "os" + "strings" + + "go.arsenm.dev/logger/log" + "go.arsenm.dev/lure/internal/repos" +) + +func handleWebhook(next http.Handler, sigCh chan<- struct{}) http.Handler { + return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + if req.URL.Path == "/webhook" { + if req.Method != http.MethodPost { + res.WriteHeader(http.StatusMethodNotAllowed) + return + } + + if req.Header.Get("X-GitHub-Event") != "push" { + http.Error(res, "Only push events are accepted by this bot", http.StatusBadRequest) + return + } + + err := verifySecure(req) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + + sigCh <- struct{}{} + return + } + + next.ServeHTTP(res, req) + }) +} + +func verifySecure(req *http.Request) error { + sigStr := req.Header.Get("X-Hub-Signature-256") + sig, err := hex.DecodeString(strings.TrimPrefix(sigStr, "sha256=")) + if err != nil { + return err + } + + secretStr, ok := os.LookupEnv("LURE_API_GITHUB_SECRET") + if !ok { + return errors.New("LURE_API_GITHUB_SECRET must be set to the secret used for setting up the github webhook") + } + secret := []byte(secretStr) + + h := hmac.New(sha256.New, secret) + _, err = io.Copy(h, req.Body) + if err != nil { + return err + } + + if !hmac.Equal(h.Sum(nil), sig) { + log.Warn("Insecure webhook request"). + Str("from", req.RemoteAddr). + Bytes("sig", sig). + Bytes("hmac", h.Sum(nil)). + Send() + + return errors.New("webhook signature mismatch") + } + + return nil +} + +func repoPullWorker(ctx context.Context, sigCh <-chan struct{}) { + for { + select { + case <-sigCh: + err := repos.Pull(ctx, gdb, cfg.Repos) + if err != nil { + log.Warn("Error while pulling repositories").Err(err).Send() + } + case <-ctx.Done(): + return + } + } +}