forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package spindleresolver 2 3import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "strings" 11 "time" 12 13 "tangled.sh/tangled.sh/core/api/tangled" 14 "tangled.sh/tangled.sh/core/appview/cache" 15 "tangled.sh/tangled.sh/core/appview/idresolver" 16 17 "github.com/bluesky-social/indigo/api/atproto" 18 "github.com/bluesky-social/indigo/xrpc" 19) 20 21type ResolutionStatus string 22 23const ( 24 StatusOK ResolutionStatus = "ok" 25 StatusError ResolutionStatus = "error" 26 StatusInvalid ResolutionStatus = "invalid" 27) 28 29type Resolution struct { 30 Status ResolutionStatus `json:"status"` 31 OwnerDID string `json:"ownerDid,omitempty"` 32 VerifiedAt time.Time `json:"verifiedAt"` 33} 34 35type Resolver struct { 36 cache *cache.Cache 37 http *http.Client 38 config Config 39 idResolver *idresolver.Resolver 40} 41 42type Config struct { 43 HitTTL time.Duration 44 ErrTTL time.Duration 45 InvalidTTL time.Duration 46 Dev bool 47} 48 49func NewResolver(cache *cache.Cache, client *http.Client, config Config) *Resolver { 50 if client == nil { 51 client = &http.Client{ 52 Timeout: 2 * time.Second, 53 } 54 } 55 return &Resolver{ 56 cache: cache, 57 http: client, 58 config: config, 59 } 60} 61 62func DefaultResolver(cache *cache.Cache) *Resolver { 63 return NewResolver( 64 cache, 65 &http.Client{ 66 Timeout: 2 * time.Second, 67 }, 68 Config{ 69 HitTTL: 24 * time.Hour, 70 ErrTTL: 30 * time.Second, 71 InvalidTTL: 1 * time.Minute, 72 }, 73 ) 74} 75 76func (r *Resolver) ResolveInstance(ctx context.Context, domain string) (*Resolution, error) { 77 key := "spindle:" + domain 78 79 val, err := r.cache.Get(ctx, key).Result() 80 if err == nil { 81 var cached Resolution 82 if err := json.Unmarshal([]byte(val), &cached); err == nil { 83 return &cached, nil 84 } 85 } 86 87 resolution, ttl := r.verify(ctx, domain) 88 89 data, _ := json.Marshal(resolution) 90 r.cache.Set(ctx, key, data, ttl) 91 92 if resolution.Status == StatusOK { 93 return resolution, nil 94 } 95 96 return resolution, fmt.Errorf("verification failed: %s", resolution.Status) 97} 98 99func (r *Resolver) verify(ctx context.Context, domain string) (*Resolution, time.Duration) { 100 owner, err := r.fetchOwner(ctx, domain) 101 if err != nil { 102 return &Resolution{Status: StatusError, VerifiedAt: time.Now()}, r.config.ErrTTL 103 } 104 105 record, err := r.fetchRecord(ctx, owner, domain) 106 if err != nil { 107 return &Resolution{Status: StatusError, VerifiedAt: time.Now()}, r.config.ErrTTL 108 } 109 110 if record.Instance == domain { 111 return &Resolution{ 112 Status: StatusOK, 113 OwnerDID: owner, 114 VerifiedAt: time.Now(), 115 }, r.config.HitTTL 116 } 117 118 return &Resolution{ 119 Status: StatusInvalid, 120 OwnerDID: owner, 121 VerifiedAt: time.Now(), 122 }, r.config.InvalidTTL 123} 124 125func (r *Resolver) fetchOwner(ctx context.Context, domain string) (string, error) { 126 scheme := "https" 127 if r.config.Dev { 128 scheme = "http" 129 } 130 131 url := fmt.Sprintf("%s://%s/owner", scheme, domain) 132 req, err := http.NewRequest("GET", url, nil) 133 if err != nil { 134 return "", err 135 } 136 137 resp, err := r.http.Do(req.WithContext(ctx)) 138 if err != nil || resp.StatusCode != 200 { 139 return "", errors.New("failed to fetch /owner") 140 } 141 142 body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) // read atmost 1kb of data 143 if err != nil { 144 return "", fmt.Errorf("failed to read /owner response: %w", err) 145 } 146 147 did := strings.TrimSpace(string(body)) 148 if did == "" { 149 return "", errors.New("empty DID in /owner response") 150 } 151 152 return did, nil 153} 154 155func (r *Resolver) fetchRecord(ctx context.Context, did, rkey string) (*tangled.Spindle, error) { 156 ident, err := r.idResolver.ResolveIdent(ctx, did) 157 if err != nil { 158 return nil, err 159 } 160 161 xrpcc := xrpc.Client{ 162 Host: ident.PDSEndpoint(), 163 } 164 165 rec, err := atproto.RepoGetRecord(ctx, &xrpcc, "", tangled.SpindleNSID, did, rkey) 166 if err != nil { 167 return nil, err 168 } 169 170 out, ok := rec.Value.Val.(*tangled.Spindle) 171 if !ok { 172 return nil, fmt.Errorf("invalid record returned") 173 } 174 175 return out, nil 176}