Something went wrong. Try again.
forked from charmbracelet/soft-serve
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165package webhook
import ( "bytes" "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http"
"github.com/charmbracelet/soft-serve/git" "github.com/charmbracelet/soft-serve/pkg/db" "github.com/charmbracelet/soft-serve/pkg/db/models" "github.com/charmbracelet/soft-serve/pkg/proto" "github.com/charmbracelet/soft-serve/pkg/ssrf" "github.com/charmbracelet/soft-serve/pkg/store" "github.com/charmbracelet/soft-serve/pkg/utils" "github.com/charmbracelet/soft-serve/pkg/version" "github.com/google/go-querystring/query" "github.com/google/uuid")
// Hook is a repository webhook.type Hook struct { models.Webhook ContentType ContentType Events []Event}
// Delivery is a webhook delivery.type Delivery struct { models.WebhookDelivery Event Event}
// secureHTTPClient is an HTTP client with SSRF protection.var secureHTTPClient = ssrf.NewSecureClient()
// do sends a webhook.// Caller must close the returned body.func do(ctx context.Context, url string, method string, headers http.Header, body io.Reader) (*http.Response, error) { req, err := http.NewRequestWithContext(ctx, method, url, body) if err != nil { return nil, err }
req.Header = headers res, err := secureHTTPClient.Do(req) if err != nil { return nil, err }
return res, nil}
// SendWebhook sends a webhook event.func SendWebhook(ctx context.Context, w models.Webhook, event Event, payload interface{}) error { var buf bytes.Buffer dbx := db.FromContext(ctx) datastore := store.FromContext(ctx)
contentType := ContentType(w.ContentType) //nolint:gosec switch contentType { case ContentTypeJSON: if err := json.NewEncoder(&buf).Encode(payload); err != nil { return err } case ContentTypeForm: v, err := query.Values(payload) if err != nil { return err } buf.WriteString(v.Encode()) //nolint: errcheck default: return ErrInvalidContentType }
headers := http.Header{} headers.Add("Content-Type", contentType.String()) headers.Add("User-Agent", "SoftServe/"+version.Version) headers.Add("X-SoftServe-Event", event.String())
id, err := uuid.NewUUID() if err != nil { return err }
headers.Add("X-SoftServe-Delivery", id.String())
reqBody := buf.String() if w.Secret != "" { sig := hmac.New(sha256.New, []byte(w.Secret)) sig.Write([]byte(reqBody)) //nolint: errcheck headers.Add("X-SoftServe-Signature", "sha256="+hex.EncodeToString(sig.Sum(nil))) }
res, reqErr := do(ctx, w.URL, http.MethodPost, headers, &buf) var reqHeaders string for k, v := range headers { reqHeaders += k + ": " + v[0] + "\n" }
resStatus := 0 resHeaders := "" resBody := ""
if res != nil { resStatus = res.StatusCode for k, v := range res.Header { resHeaders += k + ": " + v[0] + "\n" }
if res.Body != nil { defer res.Body.Close() //nolint: errcheck b, err := io.ReadAll(res.Body) if err != nil { return err }
resBody = string(b) } }
return db.WrapError(datastore.CreateWebhookDelivery(ctx, dbx, id, w.ID, int(event), w.URL, http.MethodPost, reqErr, reqHeaders, reqBody, resStatus, resHeaders, resBody))}
// SendEvent sends a webhook event.func SendEvent(ctx context.Context, payload EventPayload) error { dbx := db.FromContext(ctx) datastore := store.FromContext(ctx) webhooks, err := datastore.GetWebhooksByRepoIDWhereEvent(ctx, dbx, payload.RepositoryID(), []int{int(payload.Event())}) if err != nil { return db.WrapError(err) }
for _, w := range webhooks { if err := SendWebhook(ctx, w, payload.Event(), payload); err != nil { return err } }
return nil}
func repoURL(publicURL string, repo string) string { return fmt.Sprintf("%s/%s.git", publicURL, utils.SanitizeRepo(repo))}
func getDefaultBranch(repo proto.Repository) (string, error) { branch, err := proto.RepositoryDefaultBranch(repo) // XXX: we check for ErrReferenceNotExist here because we don't want to // return an error if the repo is an empty repo. // This means that the repo doesn't have a default branch yet and this is // the first push to it. if err != nil && !errors.Is(err, git.ErrReferenceNotExist) { return "", err }
return branch, nil}