File: teams_validation.go

package info (click to toggle)
golang-github-nicholas-fedor-shoutrrr 0.13.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 5,796 kB
  • sloc: sh: 74; makefile: 62
file content (107 lines) | stat: -rw-r--r-- 3,251 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package teams

import (
	"fmt"
	"regexp"
)

// Validation constants.
const (
	UUID4Length        = 36 // Length of a UUID4 identifier
	HashLength         = 32 // Length of a hash identifier
	WebhookDomain      = `\.webhook\.office\.com`
	ExpectedComponents = 7 // Expected number of components in webhook URL (1 match + 6 captures)
	Path               = "webhookb2"
	ProviderName       = "IncomingWebhook"

	AltIDIndex      = 2 // Index of AltID in parts array
	GroupOwnerIndex = 3 // Index of GroupOwner in parts array
)

var (
	// HostValidator ensures the host matches the Teams webhook domain pattern.
	HostValidator = regexp.MustCompile(`^[a-zA-Z0-9-]+\.webhook\.office\.com$`)
	// WebhookURLValidator ensures the full webhook URL matches the Teams pattern.
	WebhookURLValidator = regexp.MustCompile(
		`^https://[a-zA-Z0-9-]+\.webhook\.office\.com/webhookb2/[0-9a-f-]{36}@[0-9a-f-]{36}/IncomingWebhook/[0-9a-f]{32}/[0-9a-f-]{36}/[^/]+$`,
	)
)

// ValidateWebhookURL ensures the webhook URL is valid before use.
func ValidateWebhookURL(url string) error {
	if !WebhookURLValidator.MatchString(url) {
		return fmt.Errorf("%w: %q", ErrInvalidWebhookURL, url)
	}

	return nil
}

// ParseAndVerifyWebhookURL extracts and validates webhook components from a URL.
func ParseAndVerifyWebhookURL(webhookURL string) ([5]string, error) {
	pattern := regexp.MustCompile(
		`https://([a-zA-Z0-9-\.]+)` + WebhookDomain + `/` + Path + `/([0-9a-f\-]{36})@([0-9a-f\-]{36})/` + ProviderName + `/([0-9a-f]{32})/([0-9a-f\-]{36})/([^/]+)`,
	)

	groups := pattern.FindStringSubmatch(webhookURL)
	if len(groups) != ExpectedComponents {
		return [5]string{}, fmt.Errorf(
			"%w: expected %d components, got %d",
			ErrInvalidWebhookComponents,
			ExpectedComponents,
			len(groups),
		)
	}

	parts := [5]string{groups[2], groups[3], groups[4], groups[5], groups[6]}
	if err := verifyWebhookParts(parts); err != nil {
		return [5]string{}, err
	}

	return parts, nil
}

// verifyWebhookParts ensures webhook components meet format requirements.
func verifyWebhookParts(parts [5]string) error {
	type partSpec struct {
		name     string
		length   int
		index    int
		optional bool
	}

	specs := []partSpec{
		{name: "group ID", length: UUID4Length, index: 0, optional: true},
		{name: "tenant ID", length: UUID4Length, index: 1, optional: true},
		{name: "altID", length: HashLength, index: AltIDIndex, optional: true},
		{name: "groupOwner", length: UUID4Length, index: GroupOwnerIndex, optional: true},
	}

	for _, spec := range specs {
		if len(parts[spec.index]) != spec.length && parts[spec.index] != "" {
			return fmt.Errorf(
				"%w: %s must be %d characters, got %d",
				ErrInvalidPartLength,
				spec.name,
				spec.length,
				len(parts[spec.index]),
			)
		}
	}

	if parts[4] == "" {
		return ErrMissingExtraID
	}

	return nil
}

// BuildWebhookURL constructs a Teams webhook URL from components.
func BuildWebhookURL(host, group, tenant, altID, groupOwner, extraID string) string {
	// Host validation moved here for clarity
	if !HostValidator.MatchString(host) {
		return "" // Will trigger ErrInvalidHostFormat in caller
	}

	return fmt.Sprintf("https://%s/%s/%s@%s/%s/%s/%s/%s",
		host, Path, group, tenant, ProviderName, altID, groupOwner, extraID)
}