Skip to content
File

Blob: tun/client/delegation.go

go98 lines
1package client
2 
3import (
4 "context"
5 "errors"
6 "fmt"
7 "time"
8 
9 "go.miragespace.co/specter/spec/protocol"
10 "go.miragespace.co/specter/spec/rpc"
11 
12 "github.com/twitchtv/twirp"
13)
14 
15var ErrDelegationUnsupported = errors.New("server does not support domain tokens")
16 
17var ErrMintAmbiguous = errors.New("mint outcome is ambiguous; list grants before retrying")
18 
19func unsupported(err error) bool {
20 var te twirp.Error
21 return errors.As(err, &te) && te.Code() == twirp.BadRoute
22}
23 
24func definite(err error) bool {
25 var local *attachmentError
26 if errors.As(err, &local) {
27 return true
28 }
29 var te twirp.Error
30 if !errors.As(err, &te) {
31 return false
32 }
33 switch te.Code() {
34 case twirp.InvalidArgument, twirp.PermissionDenied, twirp.Unauthenticated, twirp.NotFound, twirp.Malformed, twirp.Unimplemented:
35 return true
36 }
37 return false
38}
39 
40func delegationCall[V any](c *Client, ctx context.Context, retryable bool, fn func(context.Context) (V, error)) (resp V, err error) {
41 nodes := c.getConnectedNodes()
42 if len(nodes) == 0 {
43 return resp, fmt.Errorf("no rpc candidates available")
44 }
45 var last error
46 for _, node := range nodes {
47 if ctx.Err() != nil {
48 return resp, ctx.Err()
49 }
50 callCtx, cancel := context.WithTimeout(rpc.WithNode(ctx, node), rpcTimeout)
51 resp, err = fn(callCtx)
52 cancel()
53 var rpcError twirp.Error
54 // The owner's grant quota is a definite refusal; attachment capacity is
55 // still retryable for lightweight clients on a different server.
56 quota := errors.As(err, &rpcError) && rpcError.Code() == twirp.ResourceExhausted
57 if err == nil || definite(err) || quota {
58 return resp, err
59 }
60 if unsupported(err) {
61 continue
62 }
63 if !retryable {
64 return resp, fmt.Errorf("%w: %w", ErrMintAmbiguous, err)
65 }
66 last = err
67 }
68 if last != nil {
69 return resp, last
70 }
71 return resp, ErrDelegationUnsupported
72}
73 
74func (c *Client) MintDelegation(ctx context.Context, hostname string, expiresAt time.Time) (*protocol.MintDelegationResponse, error) {
75 var expiry int64
76 if !expiresAt.IsZero() {
77 expiry = expiresAt.Unix()
78 }
79 return delegationCall(c, ctx, false, func(ctx context.Context) (*protocol.MintDelegationResponse, error) {
80 return c.tunnelClient.MintDelegation(ctx, &protocol.MintDelegationRequest{
81 Hostname: hostname,
82 ExpiresAt: expiry,
83 })
84 })
85}
86 
87func (c *Client) ListDelegations(ctx context.Context) (*protocol.ListDelegationsResponse, error) {
88 return delegationCall(c, ctx, true, func(ctx context.Context) (*protocol.ListDelegationsResponse, error) {
89 return c.tunnelClient.ListDelegations(ctx, &protocol.ListDelegationsRequest{})
90 })
91}
92 
93func (c *Client) RevokeDelegation(ctx context.Context, id string) (*protocol.RevokeDelegationResponse, error) {
94 return delegationCall(c, ctx, true, func(ctx context.Context) (*protocol.RevokeDelegationResponse, error) {
95 return c.tunnelClient.RevokeDelegation(ctx, &protocol.RevokeDelegationRequest{Id: id})
96 })
97}