1package webhook23import (4 "bytes"5 "context"6 "crypto/hmac"7 "crypto/sha256"8 "encoding/hex"9 "encoding/json"10 "errors"11 "fmt"12 "io"13 "net/http"1415 "github.com/charmbracelet/soft-serve/git"16 "github.com/charmbracelet/soft-serve/pkg/db"17 "github.com/charmbracelet/soft-serve/pkg/db/models"18 "github.com/charmbracelet/soft-serve/pkg/proto"19 "github.com/charmbracelet/soft-serve/pkg/store"20 "github.com/charmbracelet/soft-serve/pkg/utils"21 "github.com/charmbracelet/soft-serve/pkg/version"22 "github.com/google/go-querystring/query"23 "github.com/google/uuid"24)2526// Hook is a repository webhook.27type Hook struct {28 models.Webhook29 ContentType ContentType30 Events []Event31}3233// Delivery is a webhook delivery.34type Delivery struct {35 models.WebhookDelivery36 Event Event37}3839// do sends a webhook.40// Caller must close the returned body.41func do(ctx context.Context, url string, method string, headers http.Header, body io.Reader) (*http.Response, error) {42 req, err := http.NewRequestWithContext(ctx, method, url, body)43 if err != nil {44 return nil, err45 }4647 req.Header = headers48 res, err := http.DefaultClient.Do(req)49 if err != nil {50 return nil, err51 }5253 return res, nil54}5556// SendWebhook sends a webhook event.57func SendWebhook(ctx context.Context, w models.Webhook, event Event, payload interface{}) error {58 var buf bytes.Buffer59 dbx := db.FromContext(ctx)60 datastore := store.FromContext(ctx)6162 contentType := ContentType(w.ContentType) //nolint:gosec63 switch contentType {64 case ContentTypeJSON:65 if err := json.NewEncoder(&buf).Encode(payload); err != nil {66 return err67 }68 case ContentTypeForm:69 v, err := query.Values(payload)70 if err != nil {71 return err72 }73 buf.WriteString(v.Encode()) // nolint: errcheck74 default:75 return ErrInvalidContentType76 }7778 headers := http.Header{}79 headers.Add("Content-Type", contentType.String())80 headers.Add("User-Agent", "SoftServe/"+version.Version)81 headers.Add("X-SoftServe-Event", event.String())8283 id, err := uuid.NewUUID()84 if err != nil {85 return err86 }8788 headers.Add("X-SoftServe-Delivery", id.String())8990 reqBody := buf.String()91 if w.Secret != "" {92 sig := hmac.New(sha256.New, []byte(w.Secret))93 sig.Write([]byte(reqBody)) // nolint: errcheck94 headers.Add("X-SoftServe-Signature", "sha256="+hex.EncodeToString(sig.Sum(nil)))95 }9697 res, reqErr := do(ctx, w.URL, http.MethodPost, headers, &buf)98 var reqHeaders string99 for k, v := range headers {100 reqHeaders += k + ": " + v[0] + "\n"101 }102103 resStatus := 0104 resHeaders := ""105 resBody := ""106107 if res != nil {108 resStatus = res.StatusCode109 for k, v := range res.Header {110 resHeaders += k + ": " + v[0] + "\n"111 }112113 if res.Body != nil {114 defer res.Body.Close() // nolint: errcheck115 b, err := io.ReadAll(res.Body)116 if err != nil {117 return err118 }119120 resBody = string(b)121 }122 }123124 return db.WrapError(datastore.CreateWebhookDelivery(ctx, dbx, id, w.ID, int(event), w.URL, http.MethodPost, reqErr, reqHeaders, reqBody, resStatus, resHeaders, resBody))125}126127// SendEvent sends a webhook event.128func SendEvent(ctx context.Context, payload EventPayload) error {129 dbx := db.FromContext(ctx)130 datastore := store.FromContext(ctx)131 webhooks, err := datastore.GetWebhooksByRepoIDWhereEvent(ctx, dbx, payload.RepositoryID(), []int{int(payload.Event())})132 if err != nil {133 return db.WrapError(err)134 }135136 for _, w := range webhooks {137 if err := SendWebhook(ctx, w, payload.Event(), payload); err != nil {138 return err139 }140 }141142 return nil143}144145func repoURL(publicURL string, repo string) string {146 return fmt.Sprintf("%s/%s.git", publicURL, utils.SanitizeRepo(repo))147}148149func getDefaultBranch(repo proto.Repository) (string, error) {150 branch, err := proto.RepositoryDefaultBranch(repo)151 // XXX: we check for ErrReferenceNotExist here because we don't want to152 // return an error if the repo is an empty repo.153 // This means that the repo doesn't have a default branch yet and this is154 // the first push to it.155 if err != nil && !errors.Is(err, git.ErrReferenceNotExist) {156 return "", err157 }158159 return branch, nil160}