This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / repoident / knoturl.go
3.6 kB 150 lines
1package repoident 2 3import ( 4 "cmp" 5 "errors" 6 "fmt" 7 "net/url" 8 "strings" 9 10 "github.com/bluesky-social/indigo/atproto/identity" 11 "github.com/samber/lo" 12) 13 14const ( 15 KnotServiceID = "tangled_knot" 16 KnotServiceType = "TangledKnot" 17 LegacyKnotServiceID = "atproto_pds" 18 LegacyKnotServiceType = "AtprotoPersonalDataServer" 19) 20 21type SchemePolicy int 22 23const ( 24 RequireHTTPS SchemePolicy = iota 25 AllowHTTP 26) 27 28func SchemeFor(allowHTTP bool) SchemePolicy { 29 return lo.Ternary(allowHTTP, AllowHTTP, RequireHTTPS) 30} 31 32var ( 33 ErrNilIdentity = errors.New("nil identity has no knot service endpoint") 34 ErrNoKnotService = errors.New("DID document declares no " + KnotServiceID + " or " + LegacyKnotServiceID + " service endpoint") 35 ErrZeroKnotURL = errors.New("zero KnotURL has no base URL to encode") 36) 37 38var defaultPorts = map[string]string{"https": "443", "http": "80"} 39 40type KnotURL struct { 41 scheme string 42 host string 43} 44 45func (k KnotURL) IsZero() bool { return k.host == "" } 46 47func (k KnotURL) Host() string { return k.host } 48 49func (k KnotURL) url() *url.URL { return &url.URL{Scheme: k.scheme, Host: k.host} } 50 51func (k KnotURL) String() string { 52 if k.IsZero() { 53 return "" 54 } 55 return k.url().String() 56} 57 58func (k KnotURL) JoinPath(elem ...string) string { 59 return k.url().JoinPath(elem...).String() 60} 61 62func (k KnotURL) MarshalText() ([]byte, error) { 63 if k.IsZero() { 64 return nil, ErrZeroKnotURL 65 } 66 return []byte(k.String()), nil 67} 68 69func (k *KnotURL) UnmarshalText(text []byte) error { 70 parsed, err := ParseKnotURL(string(text), AllowHTTP) 71 if err != nil { 72 return err 73 } 74 *k = parsed 75 return nil 76} 77 78type pathMode int 79 80const ( 81 rejectPath pathMode = iota 82 stripPath 83) 84 85func ParseKnotURL(raw string, policy SchemePolicy) (KnotURL, error) { 86 return parseBase(raw, policy, rejectPath) 87} 88 89func KnotURLFromIdentity(ident *identity.Identity, policy SchemePolicy) (KnotURL, error) { 90 if ident == nil { 91 return KnotURL{}, ErrNilIdentity 92 } 93 raw := cmp.Or( 94 typedEndpoint(ident, KnotServiceID, KnotServiceType), 95 typedEndpoint(ident, LegacyKnotServiceID, LegacyKnotServiceType), 96 ) 97 if raw == "" { 98 return KnotURL{}, ErrNoKnotService 99 } 100 return parseBase(raw, policy, stripPath) 101} 102 103func typedEndpoint(ident *identity.Identity, id, serviceType string) string { 104 service := ident.Services[id] 105 return lo.Ternary(service.Type == serviceType, service.URL, "") 106} 107 108func parseBase(raw string, policy SchemePolicy, paths pathMode) (KnotURL, error) { 109 if raw == "" { 110 return KnotURL{}, errors.New("empty knot URL") 111 } 112 u, err := url.Parse(raw) 113 if err != nil { 114 return KnotURL{}, fmt.Errorf("invalid knot URL %q: %w", raw, err) 115 } 116 if u.Hostname() == "" { 117 return KnotURL{}, fmt.Errorf("knot URL %q has no host", raw) 118 } 119 if u.User != nil { 120 return KnotURL{}, fmt.Errorf("knot URL %q has userinfo", raw) 121 } 122 if u.RawQuery != "" || u.Fragment != "" { 123 return KnotURL{}, fmt.Errorf("knot URL %q has a query or fragment", raw) 124 } 125 if paths == rejectPath && u.Path != "" && u.Path != "/" { 126 return KnotURL{}, fmt.Errorf("knot URL %q has a path", raw) 127 } 128 switch u.Scheme { 129 case "https": 130 case "http": 131 if policy != AllowHTTP { 132 return KnotURL{}, fmt.Errorf("knot URL %q must use https", raw) 133 } 134 default: 135 return KnotURL{}, fmt.Errorf("knot URL %q has unsupported scheme %q", raw, u.Scheme) 136 } 137 return KnotURL{scheme: u.Scheme, host: canonicalHost(u)}, nil 138} 139 140func canonicalHost(u *url.URL) string { 141 host := strings.ToLower(u.Host) 142 switch port := u.Port(); port { 143 case "": 144 return strings.TrimSuffix(host, ":") 145 case defaultPorts[u.Scheme]: 146 return strings.TrimSuffix(host, ":"+port) 147 default: 148 return host 149 } 150}