An atproto PDS written in Go
at v0.5.1 5.1 kB view raw
1package identity 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net" 9 "net/http" 10 "strings" 11 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 "github.com/bluesky-social/indigo/util" 14) 15 16func ResolveHandleFromTXT(ctx context.Context, handle string) (string, error) { 17 name := fmt.Sprintf("_atproto.%s", handle) 18 recs, err := net.LookupTXT(name) 19 if err != nil { 20 return "", fmt.Errorf("handle could not be resolved via txt: %w", err) 21 } 22 23 for _, rec := range recs { 24 if strings.HasPrefix(rec, "did=") { 25 maybeDid := strings.Split(rec, "did=")[1] 26 if _, err := syntax.ParseDID(maybeDid); err == nil { 27 return maybeDid, nil 28 } 29 } 30 } 31 32 return "", fmt.Errorf("handle could not be resolved via txt: no record found") 33} 34 35func ResolveHandleFromWellKnown(ctx context.Context, cli *http.Client, handle string) (string, error) { 36 ustr := fmt.Sprintf("https://%s/.well-known/atproto-did", handle) 37 req, err := http.NewRequestWithContext( 38 ctx, 39 "GET", 40 ustr, 41 nil, 42 ) 43 if err != nil { 44 return "", fmt.Errorf("handle could not be resolved via web: %w", err) 45 } 46 47 resp, err := cli.Do(req) 48 if err != nil { 49 return "", fmt.Errorf("handle could not be resolved via web: %w", err) 50 } 51 defer resp.Body.Close() 52 53 b, err := io.ReadAll(resp.Body) 54 if err != nil { 55 return "", fmt.Errorf("handle could not be resolved via web: %w", err) 56 } 57 58 if resp.StatusCode != http.StatusOK { 59 return "", fmt.Errorf("handle could not be resolved via web: invalid status code %d", resp.StatusCode) 60 } 61 62 maybeDid := string(b) 63 64 if _, err := syntax.ParseDID(maybeDid); err != nil { 65 return "", fmt.Errorf("handle could not be resolved via web: invalid did in document") 66 } 67 68 return maybeDid, nil 69} 70 71func ResolveHandle(ctx context.Context, cli *http.Client, handle string) (string, error) { 72 if cli == nil { 73 cli = util.RobustHTTPClient() 74 } 75 76 _, err := syntax.ParseHandle(handle) 77 if err != nil { 78 return "", err 79 } 80 81 if maybeDidFromTxt, err := ResolveHandleFromTXT(ctx, handle); err == nil { 82 return maybeDidFromTxt, nil 83 } 84 85 if maybeDidFromWeb, err := ResolveHandleFromWellKnown(ctx, cli, handle); err == nil { 86 return maybeDidFromWeb, nil 87 } 88 89 return "", fmt.Errorf("handle could not be resolved") 90} 91 92func DidToDocUrl(did string) (string, error) { 93 if strings.HasPrefix(did, "did:plc:") { 94 return fmt.Sprintf("https://plc.directory/%s", did), nil 95 } else if after, ok := strings.CutPrefix(did, "did:web:"); ok { 96 return fmt.Sprintf("https://%s/.well-known/did.json", after), nil 97 } else { 98 return "", fmt.Errorf("did was not a supported did type") 99 } 100} 101 102func FetchDidDoc(ctx context.Context, cli *http.Client, did string) (*DidDoc, error) { 103 if cli == nil { 104 cli = util.RobustHTTPClient() 105 } 106 107 ustr, err := DidToDocUrl(did) 108 if err != nil { 109 return nil, err 110 } 111 112 req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil) 113 if err != nil { 114 return nil, err 115 } 116 117 resp, err := cli.Do(req) 118 if err != nil { 119 return nil, err 120 } 121 defer resp.Body.Close() 122 123 if resp.StatusCode != 200 { 124 io.Copy(io.Discard, resp.Body) 125 return nil, fmt.Errorf("unable to find did doc at url. did: %s. url: %s", did, ustr) 126 } 127 128 var diddoc DidDoc 129 if err := json.NewDecoder(resp.Body).Decode(&diddoc); err != nil { 130 return nil, err 131 } 132 133 return &diddoc, nil 134} 135 136func FetchDidData(ctx context.Context, cli *http.Client, did string) (*DidData, error) { 137 if cli == nil { 138 cli = util.RobustHTTPClient() 139 } 140 141 var ustr string 142 ustr = fmt.Sprintf("https://plc.directory/%s/data", did) 143 144 req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil) 145 if err != nil { 146 return nil, err 147 } 148 149 resp, err := cli.Do(req) 150 if err != nil { 151 return nil, err 152 } 153 defer resp.Body.Close() 154 155 if resp.StatusCode != 200 { 156 io.Copy(io.Discard, resp.Body) 157 return nil, fmt.Errorf("could not find identity in plc registry") 158 } 159 160 var diddata DidData 161 if err := json.NewDecoder(resp.Body).Decode(&diddata); err != nil { 162 return nil, err 163 } 164 165 return &diddata, nil 166} 167 168func FetchDidAuditLog(ctx context.Context, cli *http.Client, did string) (DidAuditLog, error) { 169 if cli == nil { 170 cli = util.RobustHTTPClient() 171 } 172 173 var ustr string 174 ustr = fmt.Sprintf("https://plc.directory/%s/log/audit", did) 175 176 req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil) 177 if err != nil { 178 return nil, err 179 } 180 181 resp, err := http.DefaultClient.Do(req) 182 if err != nil { 183 return nil, err 184 } 185 defer resp.Body.Close() 186 187 if resp.StatusCode != 200 { 188 io.Copy(io.Discard, resp.Body) 189 return nil, fmt.Errorf("could not find identity in plc registry") 190 } 191 192 var didlog DidAuditLog 193 if err := json.NewDecoder(resp.Body).Decode(&didlog); err != nil { 194 return nil, err 195 } 196 197 return didlog, nil 198} 199 200func ResolveService(ctx context.Context, cli *http.Client, did string) (string, error) { 201 if cli == nil { 202 cli = util.RobustHTTPClient() 203 } 204 205 diddoc, err := FetchDidDoc(ctx, cli, did) 206 if err != nil { 207 return "", err 208 } 209 210 var service string 211 for _, svc := range diddoc.Service { 212 if svc.Id == "#atproto_pds" { 213 service = svc.ServiceEndpoint 214 } 215 } 216 217 if service == "" { 218 return "", fmt.Errorf("could not find atproto_pds service in identity services") 219 } 220 221 return service, nil 222}