diff --git a/bass/github.bass b/bass/github.bass index 32626c89..175ddeda 100644 --- a/bass/github.bass +++ b/bass/github.bass @@ -1,92 +1,8 @@ #!/usr/bin/env bass -(provide [event-handler start-check start-status] +(provide [start-status] (def *memos* *dir*/memos.json) - (use (.hmac) - (.strings) - (.git (linux/alpine/git))) - - ; returns a module with a GitHub webhook event handler that dispatches events - ; back to the repo that they came from - ; - ; Verifies webhook payloads with the provided webhook secret, returning an - ; error if the signature doesn't match. - ; - ; Retrieves the repository from the event payload, clones it, loads - ; project.bass from the root of the source tree, and calls (github-event) - ; with the event type, event payload, and a module providing functions for - ; interacting with GitHub (e.g. check creation). - (defn event-handler [app-id !hook-secret! !private-key!] - (module [handle] - ; accepts webhook payloads and asynchronously dispatches events - (defn handle [request respond] - (let [{:headers {:X-Github-Delivery delivery - :X-Github-Event event - :X-Hub-Signature-256 signature} - :body body} request] - (verify! body signature) - - (log "handling" :delivery delivery :event event) - (respond {:handling delivery}) - (dispatch (decode-json body) delivery event))) - - ; verifies the HMAC signature and errors if the signature is invalid - (defn verify! [body signature] - (let [[scheme claim] (strings:split signature "=")] - (if (hmac:verify scheme !hook-secret! claim body) - :ok - (error "invalid signature")))) - - ; a module for interacting with GitHub on behalf of the app - (defn gh-client [auth] - (module [check] - (defn check [thunk name sha repo] - (start-check thunk name sha repo auth)))) - - ; forwards the event to the repository it came from - ; - ; Clones the repository at its default branch so that pull requests - ; cannot just zero-out tests or introduce malicious Bass code. - ; - ; Loads project.bass from the root of the repository and calls - ; (github-event) with the event type, event payload, and a module - ; providing functions for interacting with GitHub (e.g. check creation). - (defn dispatch [payload delivery event] - (let [{:repository - {:full-name repo-name - :clone-url url - :default-branch branch - :pushed-at pushed-at} - :installation {:id inst-id}} payload - client (gh-client {:app-id app-id - :installation-id inst-id - :private-key !private-key!}) - sha (git:ls-remote url branch pushed-at) - src (git:checkout url sha) - project (load (src/project))] - (project:github-event event payload client))))) - - ; starts the thunk and reflects its status as a Check Run - (defn start-check [thunk name sha repo auth] - (let [check-run (create-check-run - name sha repo auth - :status "in_progress" - :started-at (now 0))] - (log "created check run" - :repo repo - :name name - :sha sha - :run check-run:id) - (start thunk - (fn [ok?] - (update-check-run - check-run:id repo auth - :status "completed" - :conclusion (if ok? "success" "failure") - :completed-at (now 0)) - [name ok?])))) - ; starts the thunk and reflects its status as a Commit Status (defn start-status [thunk name sha repo auth] (create-status sha repo auth {:context name @@ -136,25 +52,7 @@ (read :json) next)) - ; creates a check run - (defn create-check-run [name sha repo auth & kwargs] - (log "creating check" :repo repo :name name :sha sha) - (gh-api "POST" (str "repos/" repo "/check-runs") - (assoc {:name name :head-sha sha} & kwargs) - auth)) - - ; updates a check run with new fields (e.g. status) - (defn update-check-run [run-id repo auth & kwargs] - (log "updating check" :run run-id :payload payload) - (gh-api "PATCH" (str "repos/" repo "/check-runs/" run-id) - (list->scope kwargs) - auth)) - ; creates or updates a commit status (defn create-status [sha repo auth body] (log "creating commit status" :repo repo :sha sha :body body) - (gh-api "POST" (str "repos/" repo "/statuses/" sha) body auth)) - - ; returns the first JSON object encoded in the payload - (defn decode-json [payload] - (next (read (mkfile ./json payload) :json)))) + (gh-api "POST" (str "repos/" repo "/statuses/" sha) body auth))) diff --git a/bass/server b/bass/server deleted file mode 100755 index 73094dd5..00000000 --- a/bass/server +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bass - -(use (.http) - (*dir*/github)) - -; construct the GitHub webhook event handler -(def handler - (github:event-handler - *env*:GITHUB_APP_ID - (mask *env*:GITHUB_WEBHOOK_SECRET :github-webhook-secret) - (mask *env*:GITHUB_APP_PRIVATE_KEY :github-app-private-key))) - -; starts up a webserver for handling webhooks -(defn main [] - (let [{(:addr "0.0.0.0:6455") addr} (next *stdin* {})] - (http:listen addr serve))) - -; handles webhooks sent to /github -; -; Returns an error for any other endpoint. -(defn serve [request respond] - (case request:path - /github - (handler:handle request respond) - - unknown - (error "not found" :http-status 404 :path unknown))) diff --git a/pkg/internal/scope.go b/pkg/internal/scope.go index 98c6cb3f..cdb70578 100644 --- a/pkg/internal/scope.go +++ b/pkg/internal/scope.go @@ -2,17 +2,12 @@ package internal import ( "context" - "crypto/hmac" - "crypto/sha256" - "encoding/hex" "fmt" - "net/http" "regexp" "strings" "time" "github.com/vito/bass/pkg/bass" - "github.com/vito/bass/pkg/srv" "github.com/vito/bass/pkg/zapctx" ) @@ -39,25 +34,6 @@ func init() { Scope.Set("string-split", bass.Func("string-split", "[delim str]", strings.Split)) - Scope.Set("http-listen", - bass.Func("http-listen", "[addr handler]", func(ctx context.Context, addr string, cb bass.Combiner) error { - server := &http.Server{ - Addr: addr, - Handler: http.MaxBytesHandler(srv.Mux(&srv.CallHandler{ - Cb: cb, - RunCtx: ctx, - }), MaxBytes), - } - - go func() { - <-ctx.Done() - // just passing ctx along to immediately interrupt everything - server.Shutdown(ctx) - }() - - return server.ListenAndServe() - })) - Scope.Set("time-measure", bass.Op("time-measure", "[form]", func(ctx context.Context, cont bass.Cont, scope *bass.Scope, form bass.Value) bass.ReadyCont { before := bass.Clock.Now() @@ -68,22 +44,6 @@ func init() { })) })) - Scope.Set("hmac-verify-sha256", - bass.Func("hmac-verify-sha256", "[key claim msg]", func(key bass.Secret, claim string, msg []byte) (bool, error) { - claimSum, err := hex.DecodeString(claim) - if err != nil { - return false, err - } - - mac := hmac.New(sha256.New, key.Reveal()) - _, err = mac.Write(msg) - if err != nil { - return false, err - } - - return hmac.Equal(mac.Sum(nil), claimSum), nil - })) - Scope.Set("regexp-case", bass.Op("regexp-case", "[str & re-fn-pairs]", func(ctx context.Context, cont bass.Cont, scope *bass.Scope, haystackForm bass.Value, pairs ...bass.Value) bass.ReadyCont { if len(pairs)%2 == 1 { diff --git a/pkg/srv/call.go b/pkg/srv/call.go deleted file mode 100644 index 8bac592a..00000000 --- a/pkg/srv/call.go +++ /dev/null @@ -1,71 +0,0 @@ -package srv - -import ( - "context" - "fmt" - "net/http" - "sync" - - "github.com/vito/bass/pkg/bass" - "github.com/vito/bass/pkg/cli" -) - -type CallHandler struct { - Cb bass.Combiner - RunCtx context.Context -} - -func (handler *CallHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // this context intentionally outlives the request so webhooks can be async - ctx := handler.RunCtx - - // each handler is concurrent, so needs its own trace - ctx = bass.ForkTrace(ctx) - - request, err := requestToScope(r) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - cli.WriteError(ctx, err) - fmt.Fprintf(w, "error: %s\n", err) - return - } - - sink := bass.NewSink(bass.NewJSONSink("response", w)) - - wg := new(sync.WaitGroup) - wg.Add(1) - - var responded bool - respond := bass.Func("respond", "[response]", func(response bass.Value) error { - err := sink.PipeSink.Emit(response) - if err != nil { - return err - } - - responded = true - - wg.Done() - - return nil - }) - - go func() { - defer func() { - if !responded { - wg.Done() - } - }() - - _, err := bass.Trampoline(ctx, handler.Cb.Call(ctx, bass.NewList(request, respond), bass.NewEmptyScope(), bass.Identity)) - if err != nil { - if !responded { - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "error: %s\n", err) - } - - cli.WriteError(ctx, err) - } - }() - - wg.Wait() -} diff --git a/pkg/srv/mux.go b/pkg/srv/mux.go deleted file mode 100644 index f24a8222..00000000 --- a/pkg/srv/mux.go +++ /dev/null @@ -1,15 +0,0 @@ -package srv - -import ( - "net/http" -) - -func Mux(call *CallHandler) *http.ServeMux { - mux := http.NewServeMux() - mux.Handle("/favicon.ico", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // TODO: embed a canonical favicon - w.WriteHeader(http.StatusNotFound) - })) - mux.Handle("/", call) - return mux -} diff --git a/pkg/srv/request.go b/pkg/srv/request.go deleted file mode 100644 index 94816455..00000000 --- a/pkg/srv/request.go +++ /dev/null @@ -1,40 +0,0 @@ -package srv - -import ( - "fmt" - "io" - "net/http" - - "github.com/vito/bass/pkg/bass" -) - -func requestToScope(r *http.Request) (*bass.Scope, error) { - request := bass.NewEmptyScope() - - headers := bass.NewEmptyScope() - for k := range r.Header { - headers.Set(bass.Symbol(k), bass.String(r.Header.Get(k))) - } - request.Set("headers", headers) - - body, err := io.ReadAll(r.Body) - if err != nil { - return nil, fmt.Errorf("read body: %w", err) - } - request.Set("body", bass.String(body)) - - request.Set("path", bass.ParseFileOrDirPath(r.URL.Path).ToValue()) - - query := bass.NewEmptyScope() - for k, v := range r.URL.Query() { - vals, err := bass.ValueOf(v) - if err != nil { - return nil, fmt.Errorf("value of %v: %w", v, err) - } - - request.Set(bass.Symbol(k), vals) - } - request.Set("query", query) - - return request, nil -} diff --git a/pkg/srv/run.go b/pkg/srv/run.go deleted file mode 100644 index 46d14caf..00000000 --- a/pkg/srv/run.go +++ /dev/null @@ -1,97 +0,0 @@ -package srv - -import ( - "context" - "fmt" - "net/http" - "path" - "path/filepath" - - "github.com/opencontainers/go-digest" - "github.com/vito/bass/pkg/bass" - "github.com/vito/bass/pkg/cli" - "github.com/vito/bass/pkg/ioctx" - "github.com/vito/bass/pkg/zapctx" - "github.com/vito/progrock" - "go.uber.org/zap" -) - -type RunHandler struct { - Dir string - Env *bass.Scope - RunCtx context.Context -} - -func (handler *RunHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // this context intentionally outlives the request so webhooks can be async - ctx := handler.RunCtx - - // each handler is concurrent, so needs its own trace - ctx = bass.ForkTrace(ctx) - - script := filepath.Join(handler.Dir, filepath.FromSlash(path.Clean(r.URL.Path))) - - request, err := requestToScope(r) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - cli.WriteError(ctx, err) - fmt.Fprintf(w, "error: %s\n", err) - return - } - - dir := filepath.Dir(script) - scope := bass.NewRunScope(bass.Ground, bass.RunState{ - Dir: bass.NewHostDir(dir), - Env: bass.NewEmptyScope(handler.Env), - Stdin: bass.NewSource(bass.NewInMemorySource(request)), - Stdout: bass.NewSink(bass.NewJSONSink("response", w)), - }) - - analogousThunk := bass.Thunk{ - Cmd: bass.ThunkCmd{ - Host: &bass.HostPath{ - ContextDir: dir, - Path: bass.FileOrDirPath{ - File: &bass.FilePath{Path: filepath.Base(script)}, - }, - }, - }, - Stdin: []bass.Value{request}, - } - - name := analogousThunk.Name() - recorder := progrock.RecorderFromContext(ctx) - bassVertex := recorder.Vertex(digest.Digest(name), fmt.Sprintf("bass %s", analogousThunk.Cmdline())) - defer func() { bassVertex.Done(err) }() - - stderr := bassVertex.Stderr() - - // wire up logs to vertex - logger := bass.LoggerTo(stderr) - ctx = zapctx.ToContext(ctx, logger) - - // wire up stderr for (log), (debug), etc. - ctx = ioctx.StderrToContext(ctx, stderr) - - _, err = bass.EvalFile(ctx, scope, script) - if err != nil { - logger.Error("errored loading script", zap.Error(err)) - // TODO: this will fail if a response is already written - do we need - // something that can handle results and then an error? - w.WriteHeader(http.StatusInternalServerError) - cli.WriteError(ctx, err) - fmt.Fprintf(w, "error: %s\n", err) - return - } - - err = bass.RunMain(ctx, scope) - if err != nil { - logger.Error("errored running main", zap.Error(err)) - // TODO: this will fail if a response is already written - do we need - // something that can handle results and then an error? - w.WriteHeader(http.StatusInternalServerError) - cli.WriteError(ctx, err) - fmt.Fprintf(w, "error: %s\n", err) - return - } -} diff --git a/std/hmac.bass b/std/hmac.bass deleted file mode 100644 index 41f45948..00000000 --- a/std/hmac.bass +++ /dev/null @@ -1,6 +0,0 @@ -(defn verify [scheme expected claimed body] - (case scheme - "sha256" (hmac-verify-sha256 expected claimed body) - _ (error "unsupported HMAC scheme" - :unsupported scheme - :supported ["sha256"]))) diff --git a/std/http.bass b/std/http.bass deleted file mode 100644 index 46e0b1cb..00000000 --- a/std/http.bass +++ /dev/null @@ -1 +0,0 @@ -(def listen http-listen)