An atproto PDS written in Go
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) 14 15func ResolveHandle(ctx context.Context, handle string) (string, error) { 16 var did string 17 18 _, err := syntax.ParseHandle(handle) 19 if err != nil { 20 return "", err 21 } 22 23 recs, err := net.LookupTXT(fmt.Sprintf("_atproto.%s", handle)) 24 if err == nil { 25 for _, rec := range recs { 26 if strings.HasPrefix(rec, "did=") { 27 did = strings.Split(rec, "did=")[1] 28 break 29 } 30 } 31 } else { 32 fmt.Printf("erorr getting txt records: %v\n", err) 33 } 34 35 if did == "" { 36 req, err := http.NewRequestWithContext( 37 ctx, 38 "GET", 39 fmt.Sprintf("https://%s/.well-known/atproto-did", handle), 40 nil, 41 ) 42 if err != nil { 43 return "", nil 44 } 45 46 resp, err := http.DefaultClient.Do(req) 47 if err != nil { 48 return "", nil 49 } 50 defer resp.Body.Close() 51 52 if resp.StatusCode != http.StatusOK { 53 io.Copy(io.Discard, resp.Body) 54 return "", fmt.Errorf("unable to resolve handle") 55 } 56 57 b, err := io.ReadAll(resp.Body) 58 if err != nil { 59 return "", err 60 } 61 62 maybeDid := string(b) 63 64 if _, err := syntax.ParseDID(maybeDid); err != nil { 65 return "", fmt.Errorf("unable to resolve handle") 66 } 67 68 did = maybeDid 69 } 70 71 return did, nil 72} 73 74type DidDoc struct { 75 Context []string `json:"@context"` 76 Id string `json:"id"` 77 AlsoKnownAs []string `json:"alsoKnownAs"` 78 VerificationMethods []DidDocVerificationMethod `json:"verificationMethods"` 79 Service []DidDocService `json:"service"` 80} 81 82type DidDocVerificationMethod struct { 83 Id string `json:"id"` 84 Type string `json:"type"` 85 Controller string `json:"controller"` 86 PublicKeyMultibase string `json:"publicKeyMultibase"` 87} 88 89type DidDocService struct { 90 Id string `json:"id"` 91 Type string `json:"type"` 92 ServiceEndpoint string `json:"serviceEndpoint"` 93} 94 95type DidData struct { 96 Did string `json:"did"` 97 VerificationMethods map[string]string `json:"verificationMethods"` 98 RotationKeys []string `json:"rotationKeys"` 99 AlsoKnownAs []string `json:"alsoKnownAs"` 100 Services map[string]DidDataService `json:"services"` 101} 102 103type DidDataService struct { 104 Type string `json:"type"` 105 Endpoint string `json:"endpoint"` 106} 107 108type DidLog []DidLogEntry 109 110type DidLogEntry struct { 111 Sig string `json:"sig"` 112 Prev *string `json:"prev"` 113 Type string `json:"string"` 114 Services map[string]DidDataService `json:"services"` 115 AlsoKnownAs []string `json:"alsoKnownAs"` 116 RotationKeys []string `json:"rotationKeys"` 117 VerificationMethods map[string]string `json:"verificationMethods"` 118} 119 120type DidAuditEntry struct { 121 Did string `json:"did"` 122 Operation DidLogEntry `json:"operation"` 123 Cid string `json:"cid"` 124 Nullified bool `json:"nullified"` 125 CreatedAt string `json:"createdAt"` 126} 127 128type DidAuditLog []DidAuditEntry 129 130func FetchDidDoc(ctx context.Context, did string) (*DidDoc, error) { 131 var ustr string 132 if strings.HasPrefix(did, "did:plc:") { 133 ustr = fmt.Sprintf("https://plc.directory/%s", did) 134 } else if strings.HasPrefix(did, "did:web:") { 135 ustr = fmt.Sprintf("https://%s/.well-known/did.json", strings.TrimPrefix(did, "did:web:")) 136 } else { 137 return nil, fmt.Errorf("did was not a supported did type") 138 } 139 140 req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil) 141 if err != nil { 142 return nil, err 143 } 144 145 resp, err := http.DefaultClient.Do(req) 146 if err != nil { 147 return nil, err 148 } 149 defer resp.Body.Close() 150 151 if resp.StatusCode != 200 { 152 io.Copy(io.Discard, resp.Body) 153 return nil, fmt.Errorf("could not find identity in plc registry") 154 } 155 156 var diddoc DidDoc 157 if err := json.NewDecoder(resp.Body).Decode(&diddoc); err != nil { 158 return nil, err 159 } 160 161 return &diddoc, nil 162} 163 164func FetchDidData(ctx context.Context, did string) (*DidData, error) { 165 var ustr string 166 ustr = fmt.Sprintf("https://plc.directory/%s/data", did) 167 168 req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil) 169 if err != nil { 170 return nil, err 171 } 172 173 resp, err := http.DefaultClient.Do(req) 174 if err != nil { 175 return nil, err 176 } 177 defer resp.Body.Close() 178 179 if resp.StatusCode != 200 { 180 io.Copy(io.Discard, resp.Body) 181 return nil, fmt.Errorf("could not find identity in plc registry") 182 } 183 184 var diddata DidData 185 if err := json.NewDecoder(resp.Body).Decode(&diddata); err != nil { 186 return nil, err 187 } 188 189 return &diddata, nil 190} 191 192func FetchDidAuditLog(ctx context.Context, did string) (DidAuditLog, error) { 193 var ustr string 194 ustr = fmt.Sprintf("https://plc.directory/%s/log/audit", did) 195 196 req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil) 197 if err != nil { 198 return nil, err 199 } 200 201 resp, err := http.DefaultClient.Do(req) 202 if err != nil { 203 return nil, err 204 } 205 defer resp.Body.Close() 206 207 if resp.StatusCode != 200 { 208 io.Copy(io.Discard, resp.Body) 209 return nil, fmt.Errorf("could not find identity in plc registry") 210 } 211 212 var didlog DidAuditLog 213 if err := json.NewDecoder(resp.Body).Decode(&didlog); err != nil { 214 return nil, err 215 } 216 217 return didlog, nil 218} 219 220func ResolveService(ctx context.Context, did string) (string, error) { 221 diddoc, err := FetchDidDoc(ctx, did) 222 if err != nil { 223 return "", err 224 } 225 226 var service string 227 for _, svc := range diddoc.Service { 228 if svc.Id == "#atproto_pds" { 229 service = svc.ServiceEndpoint 230 } 231 } 232 233 if service == "" { 234 return "", fmt.Errorf("could not find atproto_pds service in identity services") 235 } 236 237 return service, nil 238}