1package did
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "net/http"
8 "strings"
9 "time"
10 "unicode"
11
12 "github.com/whyrusleeping/go-did"
13 "go.opentelemetry.io/otel"
14)
15
16var webDidDefaultTimeout = 5 * time.Second
17
18type WebResolver struct {
19 Insecure bool
20 // TODO: cache? maybe at a different layer
21
22 client http.Client
23}
24
25func (wr *WebResolver) GetDocument(ctx context.Context, didstr string) (*Document, error) {
26 if wr.client.Timeout == 0 {
27 wr.client.Timeout = webDidDefaultTimeout
28 }
29 ctx, span := otel.Tracer("did").Start(ctx, "didWebGetDocument")
30 defer span.End()
31
32 pdid, err := did.ParseDID(didstr)
33 if err != nil {
34 return nil, err
35 }
36
37 val := pdid.Value()
38 if err := checkValidDidWeb(val); err != nil {
39 return nil, err
40 }
41
42 proto := "https"
43 if wr.Insecure {
44 proto = "http"
45 }
46
47 resp, err := wr.client.Get(proto + "://" + val + "/.well-known/did.json")
48 if err != nil {
49 return nil, err
50 }
51 defer resp.Body.Close()
52
53 if resp.StatusCode != 200 {
54 return nil, fmt.Errorf("fetch did request failed (status %d): %s", resp.StatusCode, resp.Status)
55 }
56
57 var out did.Document
58 if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
59 return nil, err
60 }
61
62 return &out, nil
63}
64
65var disallowedTlds = map[string]bool{
66 "example": true,
67 "invalid": true,
68 "local": true,
69 "arpa": true,
70 "onion": true,
71 "internal": true,
72}
73
74func (wr *WebResolver) FlushCacheFor(did string) {
75 return
76}
77
78func checkValidDidWeb(val string) error {
79 // no ports or ipv6
80 if strings.Contains(val, ":") {
81 return fmt.Errorf("did:web resolver does not handle ports or documents at sub-paths")
82 }
83 // no trailing '.'
84 if strings.HasSuffix(val, ".") {
85 return fmt.Errorf("cannot have trailing period in hostname")
86 }
87
88 parts := strings.Split(val, ".")
89 if len(parts) == 1 {
90 return fmt.Errorf("no bare hostnames (must have subdomain)")
91 }
92
93 tld := parts[len(parts)-1]
94 if disallowedTlds[tld] {
95 return fmt.Errorf("domain cannot use any disallowed TLD")
96 }
97
98 // disallow tlds that start with numbers
99 if unicode.IsNumber(rune(tld[0])) {
100 return fmt.Errorf("TLD cannot start with a number")
101 }
102
103 return nil
104}