-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfetcher.go
More file actions
133 lines (106 loc) · 3.05 KB
/
Copy pathfetcher.go
File metadata and controls
133 lines (106 loc) · 3.05 KB
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// SPDX-FileCopyrightText: 2025 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0
package dnstxtjwt
import (
"context"
"errors"
"time"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
)
// Fetcher is the main structure for this package. It holds the configuration
// for the DNS TXT record to fetch and validate.
type Fetcher struct {
// fqdn is the 'device_id.base_url' based on the input configuration.
fqdn string
// resolver is used to supply the resolver to use
resolver Resolver
// timeout is the timeout for the DNS query.
timeout time.Duration
// opts is the list of options to use for JWT validation.
opts []jwt.ParseOption
}
// Resolver is the interface that the DNS resolver must implement.
type Resolver interface {
// LookupTXT returns the DNS TXT records for the given domain name.
LookupTXT(context.Context, string) ([]string, error)
}
// FetcherOption is the interface that all options must implement.
type FetcherOption interface {
apply(*Fetcher) error
}
// New creates a new Record with the given options.
func New(opts ...FetcherOption) (*Fetcher, error) {
var r Fetcher
defaults := []FetcherOption{ // nolint:prealloc
WithResolver(nil),
WithTimeout(0),
}
vadors := []FetcherOption{ // nolint:prealloc
validateOptions(),
}
opts = append(defaults, opts...)
opts = append(opts, vadors...)
for _, opt := range opts {
if opt != nil {
if err := opt.apply(&r); err != nil {
return nil, err
}
}
}
return &r, nil
}
// Fetch retrieves the DNS TXT record and validates it as a JWT based on the
// options provided. Options for validation should be set with the
// WithParseOptions function.
func (r *Fetcher) Fetch(ctx context.Context) (jwt.Token, []byte, error) {
lines, err := r.fetch(ctx)
if err != nil {
return nil, nil, err
}
txt := reassemble(lines)
return r.verify(ctx, txt)
}
func (r Fetcher) fetch(ctx context.Context) ([]string, error) {
if r.timeout > 0 {
var cancel context.CancelFunc
// Don't wait forever if things are broken.
ctx, cancel = context.WithTimeout(ctx, r.timeout)
defer cancel()
}
txtChan := make(chan []string)
errChan := make(chan error)
go func() {
lines, err := r.resolver.LookupTXT(ctx, r.fqdn)
if err != nil {
errChan <- err
return
}
txtChan <- lines
}()
select {
case lines := <-txtChan:
return lines, nil
case err := <-errChan:
return nil, err
case <-ctx.Done():
return nil, ctx.Err()
}
}
// verify is a helper function to verify the JWT and return the token with the
// payload as bytes.
func (r *Fetcher) verify(ctx context.Context, txt string) (jwt.Token, []byte, error) {
input := []byte(txt)
opts := append(r.opts, jwt.WithContext(ctx))
token, err := jwt.Parse(input, opts...)
if err != nil {
return nil, nil, errors.Join(err, ErrInvalidJWT)
}
// Now get the payload as bytes for the return value
msg, err := jws.Parse(input)
if err != nil {
// I don't think this can happen, but just in case.
return nil, nil, errors.Join(err, ErrInvalidJWT)
}
return token, msg.Payload(), nil
}