github.com/storacha/go-ucanto@v0.7.2/server/retrieval/server.go (about)

     1  package retrieval
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/url"
    11  	"strings"
    12  	"time"
    13  
    14  	ipldprime "github.com/ipld/go-ipld-prime"
    15  	"github.com/ipld/go-ipld-prime/codec/dagjson"
    16  	"github.com/storacha/go-ucanto/core/dag/blockstore"
    17  	"github.com/storacha/go-ucanto/core/delegation"
    18  	"github.com/storacha/go-ucanto/core/invocation"
    19  	"github.com/storacha/go-ucanto/core/ipld"
    20  	"github.com/storacha/go-ucanto/core/message"
    21  	"github.com/storacha/go-ucanto/core/receipt"
    22  	"github.com/storacha/go-ucanto/core/receipt/ran"
    23  	"github.com/storacha/go-ucanto/core/result"
    24  	"github.com/storacha/go-ucanto/core/result/failure"
    25  	"github.com/storacha/go-ucanto/principal"
    26  	"github.com/storacha/go-ucanto/server"
    27  	"github.com/storacha/go-ucanto/server/transaction"
    28  	"github.com/storacha/go-ucanto/transport"
    29  	"github.com/storacha/go-ucanto/transport/headercar"
    30  	hcmsg "github.com/storacha/go-ucanto/transport/headercar/message"
    31  	thttp "github.com/storacha/go-ucanto/transport/http"
    32  	"github.com/storacha/go-ucanto/ucan"
    33  )
    34  
    35  type Request struct {
    36  	// Relative URL requested.
    37  	URL *url.URL
    38  	// Headers are the HTTP headers sent in the HTTP request to the server.
    39  	Headers http.Header
    40  }
    41  
    42  type Response struct {
    43  	// Status is the HTTP status that should be returned. e.g. 206 when returning
    44  	// a range request.
    45  	Status int
    46  	// Headers are additional HTTP headers to return in the response. At minimum
    47  	// they should include the Content-Length header, but should also include
    48  	// Content-Range for byte range responses.
    49  	Headers http.Header
    50  	// Body is the data to return in the response body.
    51  	Body io.ReadCloser
    52  }
    53  
    54  func NewResponse(status int, headers http.Header, body io.ReadCloser) Response {
    55  	return Response{Status: status, Headers: headers, Body: body}
    56  }
    57  
    58  // ServiceMethod is an invocation handler. It is different to
    59  // [server.ServiceMethod] in that it allows an [Response] to be
    60  // returned as part of the [transation.Transation], which for a retrieval server
    61  // will determine the HTTP headers and body content of the HTTP response. The
    62  // usual handler response (out and effects) are added to the X-Agent-Message
    63  // HTTP header.
    64  type ServiceMethod[O ipld.Builder, X failure.IPLDBuilderFailure] func(
    65  	context.Context,
    66  	invocation.Invocation,
    67  	server.InvocationContext,
    68  	Request,
    69  ) (transaction.Transaction[O, X], Response, error)
    70  
    71  // Service is a mapping of service names to handlers, used to define a
    72  // service implementation.
    73  type Service = map[ucan.Ability]ServiceMethod[ipld.Builder, failure.IPLDBuilderFailure]
    74  
    75  // CachingServer is a retrieval server that also caches invocations/delegations
    76  // to allow invocations with delegations chains bigger than HTTP header size
    77  // limits to be executed as multiple requests.
    78  type CachingServer interface {
    79  	server.Server[Service]
    80  	Cache() delegation.Store
    81  }
    82  
    83  // NewServer creates a retrieval server, which is a UCAN server that comes
    84  // pre-loaded with a [headercar] codec.
    85  //
    86  // Handlers have an additional return value - the data to return in the body
    87  // of the response as well as HTTP headers and status code. They also have an
    88  // additional parameter, which are the details of the request - the URL that was
    89  // requested and the HTTP headers.
    90  //
    91  // They require a delegation cache, which allows delegations that are too big
    92  // for the header to be sent in multiple rounds. By default an in-memory cache
    93  // is provided if none is passed in options.
    94  //
    95  // The carheader codec will accept agent messages where the invocation is a CID
    96  // that can be looked up in the delegations cache.
    97  //
    98  // The delegations cache should be a size bounded LRU to prevent DoS attacks.
    99  func NewServer(id principal.Signer, options ...Option) (*Server, error) {
   100  	cfg := srvConfig{service: Service{}}
   101  	for _, opt := range options {
   102  		if err := opt(&cfg); err != nil {
   103  			return nil, err
   104  		}
   105  	}
   106  
   107  	dlgCache := cfg.delegationCache
   108  	if dlgCache == nil {
   109  		dc, err := NewMemoryDelegationCache(MemoryDelegationCacheSize)
   110  		if err != nil {
   111  			return nil, err
   112  		}
   113  		dlgCache = dc
   114  	}
   115  
   116  	codec := headercar.NewInboundCodec()
   117  	srvOpts := []server.Option{server.WithInboundCodec(codec)}
   118  	if cfg.canIssue != nil {
   119  		srvOpts = append(srvOpts, server.WithCanIssue(cfg.canIssue))
   120  	}
   121  	if cfg.catch != nil {
   122  		srvOpts = append(srvOpts, server.WithErrorHandler(cfg.catch))
   123  	}
   124  	if cfg.logReceipt != nil {
   125  		srvOpts = append(srvOpts, server.WithReceiptLogger(cfg.logReceipt))
   126  	}
   127  	if cfg.validateAuthorization != nil {
   128  		srvOpts = append(srvOpts, server.WithRevocationChecker(cfg.validateAuthorization))
   129  	}
   130  	if cfg.resolveProof != nil {
   131  		srvOpts = append(srvOpts, server.WithProofResolver(cfg.resolveProof))
   132  	}
   133  	if cfg.parsePrincipal != nil {
   134  		srvOpts = append(srvOpts, server.WithPrincipalParser(cfg.parsePrincipal))
   135  	}
   136  	if cfg.resolveDIDKey != nil {
   137  		srvOpts = append(srvOpts, server.WithPrincipalResolver(cfg.resolveDIDKey))
   138  	}
   139  	if len(cfg.authorityProofs) > 0 {
   140  		srvOpts = append(srvOpts, server.WithAuthorityProofs(cfg.authorityProofs...))
   141  	}
   142  
   143  	srv, err := server.NewServer(id, srvOpts...)
   144  	if err != nil {
   145  		return nil, fmt.Errorf("creating server: %w", err)
   146  	}
   147  
   148  	return &Server{
   149  		server:          srv,
   150  		service:         cfg.service,
   151  		delegationCache: dlgCache,
   152  	}, nil
   153  }
   154  
   155  type Server struct {
   156  	server          server.Server[server.Service]
   157  	service         Service
   158  	delegationCache delegation.Store
   159  }
   160  
   161  func (srv *Server) ID() principal.Signer {
   162  	return srv.server.ID()
   163  }
   164  
   165  func (srv *Server) Service() Service {
   166  	return srv.service
   167  }
   168  
   169  func (srv *Server) Context() server.InvocationContext {
   170  	return srv.server.Context()
   171  }
   172  
   173  func (srv *Server) Codec() transport.InboundCodec {
   174  	return srv.server.Codec()
   175  }
   176  
   177  // Request handles an inbound HTTP request to the retrieval server. The request
   178  // URL will only be non-empty if this method is called with a request that is a
   179  // [transport.InboundHTTPRequest].
   180  func (srv *Server) Request(ctx context.Context, request transport.HTTPRequest) (transport.HTTPResponse, error) {
   181  	return Handle(ctx, srv, request)
   182  }
   183  
   184  func (srv *Server) Run(ctx context.Context, invocation server.ServiceInvocation) (receipt.AnyReceipt, error) {
   185  	rcpt, _, err := Run(ctx, srv, invocation, Request{})
   186  	return rcpt, err
   187  }
   188  
   189  func (srv *Server) Cache() delegation.Store {
   190  	return srv.delegationCache
   191  }
   192  
   193  func (srv *Server) Catch(err server.HandlerExecutionError[any]) {
   194  	srv.server.Catch(err)
   195  }
   196  
   197  func (srv *Server) LogReceipt(ctx context.Context, rcpt receipt.AnyReceipt, inv invocation.Invocation) error {
   198  	return srv.server.LogReceipt(ctx, rcpt, inv)
   199  }
   200  
   201  var _ CachingServer = (*Server)(nil)
   202  
   203  func Handle(ctx context.Context, srv CachingServer, request transport.HTTPRequest) (transport.HTTPResponse, error) {
   204  	resp, err := handle(ctx, srv, request)
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  
   209  	// ensure headers are not nil
   210  	headers := resp.Headers()
   211  	if headers == nil {
   212  		headers = http.Header{}
   213  	} else {
   214  		headers = headers.Clone()
   215  	}
   216  	// ensure the Vary header is set for ALL responses
   217  	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Vary
   218  	headers.Add("Vary", hcmsg.HeaderName)
   219  
   220  	if resp.Body() == nil {
   221  		return thttp.NewResponse(resp.Status(), http.NoBody, headers), nil
   222  	}
   223  	return thttp.NewResponse(resp.Status(), resp.Body(), headers), nil
   224  }
   225  
   226  func handle(ctx context.Context, srv CachingServer, request transport.HTTPRequest) (transport.HTTPResponse, error) {
   227  	selection, aerr := srv.Codec().Accept(request)
   228  	if aerr != nil {
   229  		return thttp.NewResponse(aerr.Status(), io.NopCloser(strings.NewReader(aerr.Error())), aerr.Headers()), nil
   230  	}
   231  
   232  	msg, err := selection.Decoder().Decode(request)
   233  	if err != nil {
   234  		msg := fmt.Sprintf("The server failed to decode the request payload. Please format the payload according to the specified media type: %s", err.Error())
   235  		return thttp.NewResponse(http.StatusBadRequest, io.NopCloser(strings.NewReader(msg)), nil), nil
   236  	}
   237  
   238  	// retrieval server supports only 1 invocation in the agent message, since
   239  	// only a single handler can use the body.
   240  	invs := msg.Invocations()
   241  	if len(invs) != 1 {
   242  		var rcpts []receipt.AnyReceipt
   243  		res := result.NewFailure(NewAgentMessageInvocationCountError())
   244  		for _, l := range invs {
   245  			rcpt, err := receipt.Issue(srv.ID(), res, ran.FromLink(l))
   246  			if err != nil {
   247  				return nil, fmt.Errorf("issuing invocation error receipt: %w", err)
   248  			}
   249  			rcpts = append(rcpts, rcpt)
   250  		}
   251  		out, err := message.Build(nil, rcpts)
   252  		if err != nil {
   253  			return nil, fmt.Errorf("building invocation error message: %w", err)
   254  		}
   255  		resp, err := selection.Encoder().Encode(out)
   256  		if err != nil {
   257  			return nil, fmt.Errorf("encoding invocation error message: %w", err)
   258  		}
   259  		return resp, nil
   260  	}
   261  
   262  	retreq := Request{Headers: request.Headers()}
   263  	if inreq, ok := request.(transport.InboundHTTPRequest); ok {
   264  		retreq.URL = inreq.URL()
   265  	}
   266  
   267  	out, execResp, err := Execute(ctx, srv, msg, retreq)
   268  	if err != nil {
   269  		return nil, fmt.Errorf("executing invocations: %w", err)
   270  	}
   271  
   272  	// if there is no agent message to respond with, we simply respond with the
   273  	// response from execution (i.e. missing proofs response)
   274  	if out == nil {
   275  		return thttp.NewResponse(execResp.Status, execResp.Body, execResp.Headers), nil
   276  	}
   277  
   278  	encResp, err := selection.Encoder().Encode(out)
   279  	if err != nil {
   280  		return nil, fmt.Errorf("encoding response message: %w", err)
   281  	}
   282  
   283  	// Use status from execution response if non-zero and encode response status
   284  	// is zero or 200.
   285  	status := encResp.Status()
   286  	if execResp.Status != 0 && (status == 0 || status == http.StatusOK) {
   287  		status = execResp.Status
   288  	}
   289  
   290  	// Merge headers
   291  	headers := encResp.Headers()
   292  	if execResp.Headers != nil {
   293  		if headers == nil {
   294  			headers = http.Header{}
   295  		}
   296  		for name, values := range execResp.Headers {
   297  			for _, v := range values {
   298  				headers.Add(name, v)
   299  			}
   300  		}
   301  	}
   302  
   303  	return thttp.NewResponse(status, execResp.Body, headers), nil
   304  }
   305  
   306  func Execute(ctx context.Context, srv CachingServer, msg message.AgentMessage, req Request) (message.AgentMessage, Response, error) {
   307  	// retrieval server supports only 1 invocation in the agent message, since
   308  	// only a single handler can use the body.
   309  	invs := msg.Invocations()
   310  	if len(invs) != 1 {
   311  		var rcpts []receipt.AnyReceipt
   312  		res := result.NewFailure(NewAgentMessageInvocationCountError())
   313  		for _, l := range invs {
   314  			rcpt, err := receipt.Issue(srv.ID(), res, ran.FromLink(l))
   315  			if err != nil {
   316  				return nil, Response{}, err
   317  			}
   318  			rcpts = append(rcpts, rcpt)
   319  		}
   320  		out, err := message.Build(nil, rcpts)
   321  		if err != nil {
   322  			return nil, Response{}, err
   323  		}
   324  		return out, Response{Status: http.StatusBadRequest}, nil
   325  	}
   326  
   327  	inv, err := ExtractInvocation(ctx, invs[0], msg, srv.Cache())
   328  	if err != nil {
   329  		mpe := MissingProofs{}
   330  		if errors.As(err, &mpe) {
   331  			n, err := mpe.ToIPLD()
   332  			if err != nil {
   333  				return nil, Response{}, fmt.Errorf("building missing proofs IPLD view: %w", err)
   334  			}
   335  			body, err := ipldprime.Encode(n, dagjson.Encode)
   336  			if err != nil {
   337  				return nil, Response{}, fmt.Errorf("encoding missing proofs repsonse: %w", err)
   338  			}
   339  			headers := http.Header{}
   340  			expiry := time.Now().Add(10 * time.Minute).Unix() // TODO: honour this?
   341  			headers.Set("X-UCAN-Cache-Expiry", fmt.Sprintf("%d", expiry))
   342  			headers.Set("Content-Type", "application/json")
   343  			return nil, Response{
   344  				Status:  http.StatusNotExtended,
   345  				Body:    io.NopCloser(bytes.NewReader(body)),
   346  				Headers: headers,
   347  			}, nil
   348  		}
   349  		return nil, Response{}, err
   350  	}
   351  
   352  	rcpt, resp, err := Run(ctx, srv, inv, req)
   353  	if err != nil {
   354  		return nil, Response{}, fmt.Errorf("running invocation: %w", err)
   355  	}
   356  	out, err := message.Build(nil, []receipt.AnyReceipt{rcpt})
   357  	if err != nil {
   358  		return nil, Response{}, fmt.Errorf("building agent message: %w", err)
   359  	}
   360  	return out, resp, nil
   361  }
   362  
   363  func ExtractInvocation(ctx context.Context, root ipld.Link, msg message.AgentMessage, cache delegation.Store) (invocation.Invocation, error) {
   364  	bs, err := blockstore.NewBlockStore(blockstore.WithBlocksIterator(msg.Blocks()))
   365  	if err != nil {
   366  		return nil, fmt.Errorf("creating blockstore from agent message: %w", err)
   367  	}
   368  
   369  	var dlgs []delegation.Delegation
   370  	var newdlgs []delegation.Delegation
   371  	chkpfs := []ipld.Link{root}
   372  	var missingpfs []ipld.Link
   373  	for len(chkpfs) > 0 {
   374  		prf := chkpfs[0]
   375  		chkpfs = chkpfs[1:]
   376  
   377  		blk, ok, err := bs.Get(prf)
   378  		if err != nil {
   379  			return nil, fmt.Errorf("getting block %s: %w", prf.String(), err)
   380  		}
   381  
   382  		var dlg delegation.Delegation
   383  		if ok {
   384  			dlg, err = delegation.NewDelegation(blk, bs)
   385  			if err != nil {
   386  				return nil, fmt.Errorf("creating delegation %s: %w", prf.String(), err)
   387  			}
   388  			newdlgs = append(newdlgs, dlg)
   389  		} else {
   390  			dlg, ok, err = cache.Get(ctx, prf)
   391  			if err != nil {
   392  				return nil, fmt.Errorf("getting delegation %s from cache: %w", prf.String(), err)
   393  			}
   394  			if !ok {
   395  				missingpfs = append(missingpfs, prf)
   396  				continue
   397  			}
   398  		}
   399  
   400  		dlgs = append(dlgs, dlg)
   401  		chkpfs = append(chkpfs, dlg.Proofs()...)
   402  	}
   403  
   404  	if len(missingpfs) > 0 {
   405  		for _, dlg := range newdlgs {
   406  			err := cache.Put(ctx, dlg) // cache new delegations for subsequent request
   407  			if err != nil {
   408  				return nil, fmt.Errorf("caching delegation %s: %w", dlg.Link().String(), err)
   409  			}
   410  		}
   411  		return nil, NewMissingProofsError(missingpfs)
   412  	}
   413  
   414  	// Add the blocks from the delegations to the blockstore and create a new
   415  	// invocation which has everything.
   416  	for _, dlg := range dlgs {
   417  		for b, err := range dlg.Export() {
   418  			if err != nil {
   419  				return nil, fmt.Errorf("exporting blocks from delegation %s: %w", dlg.Link().String(), err)
   420  			}
   421  			err = bs.Put(b)
   422  			if err != nil {
   423  				return nil, fmt.Errorf("putting block %s: %w", b.Link().String(), err)
   424  			}
   425  		}
   426  	}
   427  	return invocation.NewInvocationView(root, bs)
   428  }
   429  
   430  // Run is similar to [server.Run] except the receipts that are issued do not
   431  // include the invocation block(s) in order to save bytes when transmitting the
   432  // receipt in HTTP headers.
   433  func Run(ctx context.Context, srv server.Server[Service], invocation server.ServiceInvocation, req Request) (receipt.AnyReceipt, Response, error) {
   434  	caps := invocation.Capabilities()
   435  	// Invocation needs to have one single capability
   436  	if len(caps) != 1 {
   437  		capErr := server.NewInvocationCapabilityError(invocation.Capabilities())
   438  		rcpt, err := receipt.Issue(srv.ID(), result.NewFailure(capErr), ran.FromLink(invocation.Link()))
   439  		return rcpt, Response{}, err
   440  	}
   441  
   442  	cap := caps[0]
   443  	handle, ok := srv.Service()[cap.Can()]
   444  	if !ok {
   445  		notFoundErr := server.NewHandlerNotFoundError(cap)
   446  		rcpt, err := receipt.Issue(srv.ID(), result.NewFailure(notFoundErr), ran.FromLink(invocation.Link()))
   447  		return rcpt, Response{}, err
   448  	}
   449  
   450  	tx, resp, err := handle(ctx, invocation, srv.Context(), req)
   451  	if err != nil {
   452  		if errors.Is(err, context.Canceled) {
   453  			return nil, Response{}, err
   454  		}
   455  		execErr := server.NewHandlerExecutionError(err, cap)
   456  		srv.Catch(execErr)
   457  		rcpt, err := receipt.Issue(srv.ID(), result.NewFailure(execErr), ran.FromLink(invocation.Link()))
   458  		if err != nil {
   459  			return nil, Response{}, err
   460  		}
   461  		return rcpt, resp, nil
   462  	}
   463  
   464  	fx := tx.Fx()
   465  	var opts []receipt.Option
   466  	if fx != nil {
   467  		opts = append(opts, receipt.WithJoin(fx.Join()), receipt.WithFork(fx.Fork()...))
   468  	}
   469  
   470  	rcpt, err := receipt.Issue(srv.ID(), tx.Out(), ran.FromLink(invocation.Link()), opts...)
   471  	if err != nil {
   472  		return nil, Response{}, err
   473  	}
   474  
   475  	if err := srv.LogReceipt(ctx, rcpt, invocation); err != nil {
   476  		return nil, Response{}, err
   477  	}
   478  
   479  	return rcpt, resp, nil
   480  }