1package webhook
  2
  3import (
  4	"bytes"
  5	"context"
  6	"crypto/hmac"
  7	"crypto/sha256"
  8	"encoding/hex"
  9	"encoding/json"
 10	"fmt"
 11	"io"
 12	"net/http"
 13
 14	"github.com/charmbracelet/soft-serve/pkg/db"
 15	"github.com/charmbracelet/soft-serve/pkg/db/models"
 16	"github.com/charmbracelet/soft-serve/pkg/store"
 17	"github.com/charmbracelet/soft-serve/pkg/utils"
 18	"github.com/charmbracelet/soft-serve/pkg/version"
 19	"github.com/google/go-querystring/query"
 20	"github.com/google/uuid"
 21)
 22
 23// Hook is a repository webhook.
 24type Hook struct {
 25	models.Webhook
 26	ContentType ContentType
 27	Events      []Event
 28}
 29
 30// Delivery is a webhook delivery.
 31type Delivery struct {
 32	models.WebhookDelivery
 33	Event Event
 34}
 35
 36// do sends a webhook.
 37// Caller must close the returned body.
 38func do(ctx context.Context, url string, method string, headers http.Header, body io.Reader) (*http.Response, error) {
 39	req, err := http.NewRequestWithContext(ctx, method, url, body)
 40	if err != nil {
 41		return nil, err
 42	}
 43
 44	req.Header = headers
 45	res, err := http.DefaultClient.Do(req)
 46	if err != nil {
 47		return nil, err
 48	}
 49
 50	return res, nil
 51}
 52
 53// SendWebhook sends a webhook event.
 54func SendWebhook(ctx context.Context, w models.Webhook, event Event, payload interface{}) error {
 55	var buf bytes.Buffer
 56	dbx := db.FromContext(ctx)
 57	datastore := store.FromContext(ctx)
 58
 59	contentType := ContentType(w.ContentType)
 60	switch contentType {
 61	case ContentTypeJSON:
 62		if err := json.NewEncoder(&buf).Encode(payload); err != nil {
 63			return err
 64		}
 65	case ContentTypeForm:
 66		v, err := query.Values(payload)
 67		if err != nil {
 68			return err
 69		}
 70		buf.WriteString(v.Encode()) // nolint: errcheck
 71	default:
 72		return ErrInvalidContentType
 73	}
 74
 75	headers := http.Header{}
 76	headers.Add("Content-Type", contentType.String())
 77	headers.Add("User-Agent", "SoftServe/"+version.Version)
 78	headers.Add("X-SoftServe-Event", event.String())
 79
 80	id, err := uuid.NewUUID()
 81	if err != nil {
 82		return err
 83	}
 84
 85	headers.Add("X-SoftServe-Delivery", id.String())
 86
 87	reqBody := buf.String()
 88	if w.Secret != "" {
 89		sig := hmac.New(sha256.New, []byte(w.Secret))
 90		sig.Write([]byte(reqBody)) // nolint: errcheck
 91		headers.Add("X-SoftServe-Signature", "sha256="+hex.EncodeToString(sig.Sum(nil)))
 92	}
 93
 94	res, reqErr := do(ctx, w.URL, http.MethodPost, headers, &buf)
 95	var reqHeaders string
 96	for k, v := range headers {
 97		reqHeaders += k + ": " + v[0] + "\n"
 98	}
 99
100	resStatus := 0
101	resHeaders := ""
102	resBody := ""
103
104	if res != nil {
105		resStatus = res.StatusCode
106		for k, v := range res.Header {
107			resHeaders += k + ": " + v[0] + "\n"
108		}
109
110		if res.Body != nil {
111			defer res.Body.Close() // nolint: errcheck
112			b, err := io.ReadAll(res.Body)
113			if err != nil {
114				return err
115			}
116
117			resBody = string(b)
118		}
119	}
120
121	return db.WrapError(datastore.CreateWebhookDelivery(ctx, dbx, id, w.ID, int(event), w.URL, http.MethodPost, reqErr, reqHeaders, reqBody, resStatus, resHeaders, resBody))
122}
123
124// SendEvent sends a webhook event.
125func SendEvent(ctx context.Context, payload EventPayload) error {
126	dbx := db.FromContext(ctx)
127	datastore := store.FromContext(ctx)
128	webhooks, err := datastore.GetWebhooksByRepoIDWhereEvent(ctx, dbx, payload.RepositoryID(), []int{int(payload.Event())})
129	if err != nil {
130		return db.WrapError(err)
131	}
132
133	for _, w := range webhooks {
134		if err := SendWebhook(ctx, w, payload.Event(), payload); err != nil {
135			return err
136		}
137	}
138
139	return nil
140}
141
142func repoURL(publicURL string, repo string) string {
143	return fmt.Sprintf("%s/%s.git", publicURL, utils.SanitizeRepo(repo))
144}