Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
6.2 kB · 185 lines
Go
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186package repoverify
import ( "context" "errors" "fmt" "net/http" "strings" "time"
"github.com/bluesky-social/indigo/atproto/syntax" lexutil "github.com/bluesky-social/indigo/lex/util" indigoxrpc "github.com/bluesky-social/indigo/xrpc" "github.com/samber/lo" orgtangled "tangled.org/core/api/org_tangled" "tangled.org/core/api/tangled" "tangled.org/core/hostutil" "tangled.org/core/idresolver" "tangled.org/core/repoident" "tangled.org/core/xrpc/xrpcclient")
type Answer string
const ( AnswerUnset Answer = "unset" AnswerOwner Answer = "owner" AnswerNoRoute Answer = "noRoute" AnswerAbsent Answer = "absent")
type Ownership struct { OwnerDid repoident.OwnerDid // old go knots have no name, so there it is the sh.tangled.repo rkey Slug syntax.RecordKey Name string}
type Result struct { RepoDid repoident.RepoDid KnotURL repoident.KnotURL
answer Answer ownership Ownership}
func Owned(repoDid repoident.RepoDid, knot repoident.KnotURL, ownership Ownership) Result { return Result{RepoDid: repoDid, KnotURL: knot, answer: AnswerOwner, ownership: ownership}}
func Refused(repoDid repoident.RepoDid, knot repoident.KnotURL, answer Answer) Result { return Result{RepoDid: repoDid, KnotURL: knot, answer: answer}}
func (r Result) Answer() Answer { return lo.Ternary(r.answer == "", AnswerUnset, r.answer)}
func (r Result) Ownership() (Ownership, bool) { return r.ownership, r.answer == AnswerOwner}
var ErrKnotAnswer = errors.New("knot returned an invalid ownership answer")
const ( repoNotFound = "RepoNotFound" recordNotFound = "RecordNotFound")
var terminalErrors = []error{ ErrKnotAnswer, repoident.ErrNoKnotService, repoident.ErrNilIdentity, xrpcclient.ErrXrpcUnauthorized, xrpcclient.ErrXrpcForbidden,}
func Retriable(err error) bool { return err != nil && !lo.ContainsBy(terminalErrors, func(t error) bool { return errors.Is(err, t) })}
const getRecordNSID = "com.atproto.repo.getRecord"
// decodes only the declaration, so a manifest field we don't know yet can't fail the readtype manifestRecord struct { URI string `json:"uri"` Value struct { Declaration *orgtangled.RepoManifest_Declaration `json:"declaration"` } `json:"value"`}
func Describe(ctx context.Context, httpClient *http.Client, knot repoident.KnotURL, repoDid repoident.RepoDid) (Result, error) { client := &indigoxrpc.Client{Host: knot.String(), Client: httpClient} var out manifestRecord params := map[string]any{ "repo": repoDid.String(), "collection": orgtangled.RepoManifestNSID, "rkey": "self", } err := client.LexDo(ctx, lexutil.Query, "", getRecordNSID, params, nil, &out) switch xrpcclient.ErrorName(err) { case repoNotFound: return Refused(repoDid, knot, AnswerAbsent), nil case recordNotFound: return Refused(repoDid, knot, AnswerUnset), nil } if xrpcErr := xrpcclient.HandleXrpcErr(err); xrpcErr != nil { if noRoute(err, xrpcErr) { return describeRepo(ctx, client, knot, repoDid) } return Result{}, fmt.Errorf("manifest on %s: %w (%v)", knot, xrpcErr, err) } if uri, err := syntax.ParseATURI(out.URI); err != nil || uri.Authority().String() != repoDid.String() { return Result{}, fmt.Errorf("%w: knot %s returned manifest %q for repoDid %s", ErrKnotAnswer, knot, out.URI, repoDid) } declaration := out.Value.Declaration if declaration == nil { return Result{}, fmt.Errorf("%w: knot %s returned a manifest without a declaration", ErrKnotAnswer, knot) } ownerDid, err := repoident.NewOwnerDid(declaration.Owner) if err != nil { return Result{}, fmt.Errorf("%w from knot %s: %w", ErrKnotAnswer, knot, err) } slug, err := syntax.ParseRecordKey(strings.ToLower(declaration.Name)) if err != nil { return Result{}, fmt.Errorf("%w: knot %s returned name %q: %w", ErrKnotAnswer, knot, declaration.Name, err) } return Owned(repoDid, knot, Ownership{OwnerDid: ownerDid, Slug: slug, Name: declaration.Name}), nil}
// a 404 with no error name or a 501 is a knot without the route, a named error is a real answerfunc noRoute(err, xrpcErr error) bool { var status *indigoxrpc.Error return errors.Is(xrpcErr, xrpcclient.ErrXrpcUnsupported) || errors.Is(xrpcErr, xrpcclient.ErrXrpcNotFound) || (errors.As(err, &status) && status.StatusCode == http.StatusNotImplemented)}
func describeRepo(ctx context.Context, client *indigoxrpc.Client, knot repoident.KnotURL, repoDid repoident.RepoDid) (Result, error) { out, err := tangled.RepoDescribeRepo(ctx, client, repoDid.String()) if xrpcErr := xrpcclient.HandleXrpcErr(err); xrpcErr != nil { if noRoute(err, xrpcErr) { return Refused(repoDid, knot, lo.Ternary(xrpcclient.ErrorName(err) == repoNotFound, AnswerAbsent, AnswerNoRoute)), nil } return Result{}, fmt.Errorf("describeRepo on %s: %w (%v)", knot, xrpcErr, err) } if out.RepoDid != repoDid.String() { return Result{}, fmt.Errorf("%w: knot %s returned repoDid %q, want %q", ErrKnotAnswer, knot, out.RepoDid, repoDid) } ownerDid, err := repoident.NewOwnerDid(out.OwnerDid) if err != nil { return Result{}, fmt.Errorf("%w from knot %s: %w", ErrKnotAnswer, knot, err) } rkey, err := syntax.ParseRecordKey(out.Rkey) if err != nil { return Result{}, fmt.Errorf("%w: knot %s returned rkey %q: %w", ErrKnotAnswer, knot, out.Rkey, err) } return Owned(repoDid, knot, Ownership{OwnerDid: ownerDid, Slug: rkey, Name: rkey.String()}), nil}
type Verifier func(ctx context.Context, repoDid repoident.RepoDid) (Result, error)
const verifyTimeout = 10 * time.Second
func New(resolver *idresolver.Resolver, dev bool) Verifier { httpClient := hostutil.SafeClient(dev, verifyTimeout) policy := repoident.SchemeFor(dev)
return func(ctx context.Context, repoDid repoident.RepoDid) (Result, error) { ctx, cancel := context.WithTimeout(ctx, verifyTimeout) defer cancel()
ident, err := resolver.ResolveIdent(ctx, repoDid.String()) if err != nil { return Result{}, fmt.Errorf("resolve repoDid %s: %w", repoDid, err) }
knot, err := repoident.KnotURLFromIdentity(ident, policy) if err != nil { return Result{}, fmt.Errorf("repoDid %s: %w", repoDid, err) }
return Describe(ctx, httpClient, knot, repoDid) }}